redeploy 0.2.55__tar.gz → 0.2.57__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.
- {redeploy-0.2.55 → redeploy-0.2.57}/PKG-INFO +6 -6
- {redeploy-0.2.55 → redeploy-0.2.57}/README.md +5 -5
- {redeploy-0.2.55 → redeploy-0.2.57}/pyproject.toml +1 -1
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/__init__.py +1 -1
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/docker_compose.py +74 -49
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/parsers/compose.py +30 -18
- redeploy-0.2.57/redeploy/models/enums.py +69 -0
- redeploy-0.2.57/redeploy/models/persisted.py +27 -0
- redeploy-0.2.57/redeploy/tests/test_heal_decider.py +91 -0
- redeploy-0.2.57/redeploy/tests/test_iac_compose_contract.py +181 -0
- redeploy-0.2.57/redeploy/tests/test_iac_docker_compose_contract.py +172 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/SOURCES.txt +5 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/handlers.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/audit.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/exec_.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/prompt_cmd.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/discovery.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/fleet.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/runner.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/parser.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/mcp_server.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/models.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/observe.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/parse.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/patterns.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/schema.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/ssh.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/steps/builtins.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_heal.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/verify.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/setup.cfg +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_heal_loop_detector.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_iac.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_parse.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_prompt_llm.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_steps_kiosk.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.57
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
|
32
32
|
|
|
33
33
|
## AI Cost Tracking
|
|
34
34
|
|
|
35
|
-
     
|
|
36
|
+
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
38
|
+
- 🤖 **LLM usage:** $7.5000 (77 commits)
|
|
39
|
+
- 👤 **Human dev:** ~$2886 (28.9h @ $100/h, 30min dedup)
|
|
40
40
|
|
|
41
41
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
    
|
|
46
46
|
|
|
47
47
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
48
48
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (77 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2886 (28.9h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
|
|
|
7
7
|
|
|
8
8
|
Everything not listed in ``__all__`` is internal and may change without notice.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.57"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -291,14 +291,12 @@ class DockerComposeParser:
|
|
|
291
291
|
pass
|
|
292
292
|
return env
|
|
293
293
|
|
|
294
|
-
def
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# image / build
|
|
301
|
-
image = resolve(cfg.get("image")) or None
|
|
294
|
+
def _resolve_image_and_build(
|
|
295
|
+
self,
|
|
296
|
+
cfg: dict,
|
|
297
|
+
env_context: dict[str, str],
|
|
298
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
299
|
+
image = _resolve_var(str(cfg.get("image")), env_context) if cfg.get("image") is not None else None
|
|
302
300
|
build = cfg.get("build")
|
|
303
301
|
build_context: Optional[str] = None
|
|
304
302
|
if build:
|
|
@@ -308,8 +306,15 @@ class DockerComposeParser:
|
|
|
308
306
|
build_context = build.get("context", ".")
|
|
309
307
|
if not image:
|
|
310
308
|
image = f"<build:{build_context or '.'}>"
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
return image, build_context
|
|
310
|
+
|
|
311
|
+
def _parse_service_ports(
|
|
312
|
+
self,
|
|
313
|
+
name: str,
|
|
314
|
+
cfg: dict,
|
|
315
|
+
env_context: dict[str, str],
|
|
316
|
+
spec: ParsedSpec,
|
|
317
|
+
) -> list[PortInfo]:
|
|
313
318
|
ports: list[PortInfo] = []
|
|
314
319
|
for raw in (cfg.get("ports") or []):
|
|
315
320
|
if isinstance(raw, str):
|
|
@@ -318,75 +323,95 @@ class DockerComposeParser:
|
|
|
318
323
|
if p:
|
|
319
324
|
ports.append(p)
|
|
320
325
|
else:
|
|
321
|
-
spec.add_warning(f"[{name}] Could not parse port: {raw!r}",
|
|
322
|
-
|
|
326
|
+
spec.add_warning(f"[{name}] Could not parse port: {raw!r}", severity="warn")
|
|
327
|
+
return ports
|
|
323
328
|
|
|
324
|
-
|
|
329
|
+
def _parse_service_volumes(self, cfg: dict) -> list[VolumeInfo]:
|
|
325
330
|
volumes: list[VolumeInfo] = []
|
|
326
331
|
for raw in (cfg.get("volumes") or []):
|
|
327
332
|
v = _parse_volume(raw)
|
|
328
333
|
if v:
|
|
329
334
|
volumes.append(v)
|
|
335
|
+
return volumes
|
|
330
336
|
|
|
331
|
-
|
|
337
|
+
def _parse_service_env(
|
|
338
|
+
self,
|
|
339
|
+
cfg: dict,
|
|
340
|
+
env_context: dict[str, str],
|
|
341
|
+
) -> dict[str, str]:
|
|
332
342
|
env = _env_dict(cfg.get("environment"))
|
|
333
|
-
|
|
334
|
-
env = {k: _resolve_var(v, env_context) for k, v in env.items()}
|
|
343
|
+
return {k: _resolve_var(v, env_context) for k, v in env.items()}
|
|
335
344
|
|
|
336
|
-
|
|
337
|
-
env_files: list[str] = []
|
|
345
|
+
def _parse_service_env_files(self, cfg: dict) -> list[str]:
|
|
338
346
|
ef = cfg.get("env_file")
|
|
339
|
-
if ef:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
347
|
+
if not ef:
|
|
348
|
+
return []
|
|
349
|
+
if isinstance(ef, str):
|
|
350
|
+
return [ef]
|
|
351
|
+
if isinstance(ef, list):
|
|
352
|
+
return [str(x) for x in ef]
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
def _parse_service_networks(self, cfg: dict) -> list[str]:
|
|
346
356
|
svc_nets = cfg.get("networks")
|
|
347
357
|
if isinstance(svc_nets, dict):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
svc_networks = []
|
|
358
|
+
return list(svc_nets.keys())
|
|
359
|
+
if isinstance(svc_nets, list):
|
|
360
|
+
return [str(n) for n in svc_nets]
|
|
361
|
+
return []
|
|
353
362
|
|
|
354
|
-
|
|
363
|
+
def _parse_service_depends_on(self, cfg: dict) -> list[str]:
|
|
355
364
|
dep = cfg.get("depends_on")
|
|
356
365
|
if isinstance(dep, list):
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
depends_on = []
|
|
366
|
+
return [str(d) for d in dep]
|
|
367
|
+
if isinstance(dep, dict):
|
|
368
|
+
return list(dep.keys())
|
|
369
|
+
return []
|
|
362
370
|
|
|
363
|
-
|
|
371
|
+
def _parse_service_healthcheck(self, cfg: dict) -> Optional[str]:
|
|
364
372
|
hc = cfg.get("healthcheck")
|
|
365
|
-
healthcheck: Optional[str] = None
|
|
366
373
|
if isinstance(hc, dict):
|
|
367
374
|
test = hc.get("test")
|
|
368
375
|
if isinstance(test, list):
|
|
369
376
|
test = " ".join(str(t) for t in test)
|
|
370
|
-
|
|
377
|
+
return str(test) if test else None
|
|
378
|
+
return None
|
|
371
379
|
|
|
372
|
-
|
|
380
|
+
def _parse_service_replicas(self, cfg: dict) -> int:
|
|
373
381
|
deploy = cfg.get("deploy") or {}
|
|
374
|
-
replicas = 1
|
|
375
382
|
if isinstance(deploy, dict):
|
|
376
|
-
|
|
383
|
+
return int(deploy.get("replicas", 1))
|
|
384
|
+
return 1
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
def _parse_service_labels(self, cfg: dict) -> dict[str, str]:
|
|
379
387
|
labels_raw = cfg.get("labels")
|
|
380
388
|
if isinstance(labels_raw, dict):
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
labels = {}
|
|
389
|
+
return {str(k): str(v) for k, v in labels_raw.items()}
|
|
390
|
+
if isinstance(labels_raw, list):
|
|
391
|
+
labels: dict[str, str] = {}
|
|
384
392
|
for item in labels_raw:
|
|
385
393
|
if "=" in str(item):
|
|
386
394
|
k, v = str(item).split("=", 1)
|
|
387
395
|
labels[k] = v
|
|
388
|
-
|
|
389
|
-
|
|
396
|
+
return labels
|
|
397
|
+
return {}
|
|
398
|
+
|
|
399
|
+
def _parse_service_command(self, cfg: dict) -> Optional[str]:
|
|
400
|
+
return str(cfg["command"]) if cfg.get("command") else None
|
|
401
|
+
|
|
402
|
+
def _parse_service(self, name: str, cfg: dict,
|
|
403
|
+
env_context: dict[str, str],
|
|
404
|
+
spec: ParsedSpec) -> ServiceInfo:
|
|
405
|
+
image, build_context = self._resolve_image_and_build(cfg, env_context)
|
|
406
|
+
ports = self._parse_service_ports(name, cfg, env_context, spec)
|
|
407
|
+
volumes = self._parse_service_volumes(cfg)
|
|
408
|
+
env = self._parse_service_env(cfg, env_context)
|
|
409
|
+
env_files = self._parse_service_env_files(cfg)
|
|
410
|
+
svc_networks = self._parse_service_networks(cfg)
|
|
411
|
+
depends_on = self._parse_service_depends_on(cfg)
|
|
412
|
+
healthcheck = self._parse_service_healthcheck(cfg)
|
|
413
|
+
replicas = self._parse_service_replicas(cfg)
|
|
414
|
+
labels = self._parse_service_labels(cfg)
|
|
390
415
|
|
|
391
416
|
return ServiceInfo(
|
|
392
417
|
name=name,
|
|
@@ -399,7 +424,7 @@ class DockerComposeParser:
|
|
|
399
424
|
depends_on=depends_on,
|
|
400
425
|
healthcheck=healthcheck,
|
|
401
426
|
restart=cfg.get("restart"),
|
|
402
|
-
command=
|
|
427
|
+
command=self._parse_service_command(cfg),
|
|
403
428
|
build_context=build_context,
|
|
404
429
|
replicas=replicas,
|
|
405
430
|
labels=labels,
|
|
@@ -20,7 +20,6 @@ from typing import Any, Optional
|
|
|
20
20
|
import yaml
|
|
21
21
|
|
|
22
22
|
from ..base import (
|
|
23
|
-
ConversionWarning,
|
|
24
23
|
ParsedSpec,
|
|
25
24
|
PortInfo,
|
|
26
25
|
ServiceInfo,
|
|
@@ -91,8 +90,7 @@ class DockerComposeParser:
|
|
|
91
90
|
spec.networks.append(net_name)
|
|
92
91
|
|
|
93
92
|
# top-level volumes
|
|
94
|
-
for vol_name,
|
|
95
|
-
vol_def = vol_def or {}
|
|
93
|
+
for vol_name, _ in (raw.get("volumes") or {}).items():
|
|
96
94
|
spec.volumes.append(VolumeInfo(
|
|
97
95
|
target="",
|
|
98
96
|
source=vol_name,
|
|
@@ -113,40 +111,54 @@ class DockerComposeParser:
|
|
|
113
111
|
|
|
114
112
|
# ── service parsing ───────────────────────────────────────────────────────
|
|
115
113
|
|
|
114
|
+
def _collect_service_secrets(self, d: dict, spec: ParsedSpec) -> None:
|
|
115
|
+
for secret in (d.get("secrets") or []):
|
|
116
|
+
secret_name = secret if isinstance(secret, str) else secret.get("source", "")
|
|
117
|
+
if secret_name and secret_name not in spec.secrets_referenced:
|
|
118
|
+
spec.secrets_referenced.append(secret_name)
|
|
119
|
+
|
|
120
|
+
def _merge_service_env_files(self, d: dict, svc: ServiceInfo, spec: ParsedSpec) -> None:
|
|
121
|
+
for ef in (d.get("env_file") or []):
|
|
122
|
+
ef = ef if isinstance(ef, str) else ef.get("path", "")
|
|
123
|
+
if ef and ef not in spec.env_files:
|
|
124
|
+
spec.env_files.append(ef)
|
|
125
|
+
if ef and ef not in svc.env_files:
|
|
126
|
+
svc.env_files.append(ef)
|
|
127
|
+
|
|
128
|
+
def _collect_service_image(self, svc: ServiceInfo, spec: ParsedSpec) -> None:
|
|
129
|
+
if svc.image and svc.image not in spec.images:
|
|
130
|
+
spec.images.append(svc.image)
|
|
131
|
+
|
|
132
|
+
def _parse_service_networks(self, d: dict) -> list[str]:
|
|
133
|
+
return list(d.get("networks") or [])
|
|
134
|
+
|
|
135
|
+
def _parse_service_replicas(self, d: dict) -> int:
|
|
136
|
+
return int((d.get("deploy") or {}).get("replicas", 1))
|
|
137
|
+
|
|
116
138
|
def _parse_service(self, name: str, d: dict, spec: ParsedSpec) -> ServiceInfo:
|
|
117
139
|
svc = ServiceInfo(name=name)
|
|
118
140
|
|
|
119
141
|
svc.image = d.get("image") or None
|
|
120
|
-
|
|
121
|
-
if svc.image not in spec.images:
|
|
122
|
-
spec.images.append(svc.image)
|
|
142
|
+
self._collect_service_image(svc, spec)
|
|
123
143
|
|
|
124
144
|
svc.build_context = self._parse_build(d.get("build"))
|
|
125
145
|
svc.command = self._parse_command(d.get("command"))
|
|
126
146
|
svc.restart = d.get("restart") or None
|
|
127
|
-
svc.replicas =
|
|
147
|
+
svc.replicas = self._parse_service_replicas(d)
|
|
128
148
|
|
|
129
149
|
svc.ports = self._parse_ports(d.get("ports") or [], spec)
|
|
130
150
|
svc.volumes = self._parse_volumes(d.get("volumes") or [], spec)
|
|
131
|
-
svc.networks =
|
|
151
|
+
svc.networks = self._parse_service_networks(d)
|
|
132
152
|
svc.env, svc.env_files = self._parse_env(d, spec)
|
|
133
153
|
svc.depends_on = self._parse_depends_on(d.get("depends_on"))
|
|
134
154
|
svc.labels = self._parse_labels(d.get("labels"))
|
|
135
155
|
svc.healthcheck = self._parse_healthcheck(d.get("healthcheck"))
|
|
136
156
|
|
|
137
157
|
# secrets referenced by service
|
|
138
|
-
|
|
139
|
-
secret_name = secret if isinstance(secret, str) else secret.get("source", "")
|
|
140
|
-
if secret_name and secret_name not in spec.secrets_referenced:
|
|
141
|
-
spec.secrets_referenced.append(secret_name)
|
|
158
|
+
self._collect_service_secrets(d, spec)
|
|
142
159
|
|
|
143
160
|
# env_files at service level
|
|
144
|
-
|
|
145
|
-
ef = ef if isinstance(ef, str) else ef.get("path", "")
|
|
146
|
-
if ef and ef not in spec.env_files:
|
|
147
|
-
spec.env_files.append(ef)
|
|
148
|
-
if ef and ef not in svc.env_files:
|
|
149
|
-
svc.env_files.append(ef)
|
|
161
|
+
self._merge_service_env_files(d, svc, spec)
|
|
150
162
|
|
|
151
163
|
return svc
|
|
152
164
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shared enums and type aliases for redeploy models."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConflictSeverity(str, Enum):
|
|
8
|
+
LOW = "low"
|
|
9
|
+
MEDIUM = "medium"
|
|
10
|
+
HIGH = "high"
|
|
11
|
+
CRITICAL = "critical"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StepAction(str, Enum):
|
|
15
|
+
SYSTEMCTL_STOP = "systemctl_stop"
|
|
16
|
+
SYSTEMCTL_DISABLE = "systemctl_disable"
|
|
17
|
+
SYSTEMCTL_START = "systemctl_start"
|
|
18
|
+
KUBECTL_DELETE = "kubectl_delete"
|
|
19
|
+
DOCKER_COMPOSE_UP = "docker_compose_up"
|
|
20
|
+
DOCKER_COMPOSE_DOWN = "docker_compose_down"
|
|
21
|
+
DOCKER_BUILD = "docker_build"
|
|
22
|
+
DOCKER_HEALTH_WAIT = "docker_health_wait"
|
|
23
|
+
CONTAINER_LOG_TAIL = "container_log_tail"
|
|
24
|
+
PODMAN_BUILD = "podman_build"
|
|
25
|
+
RSYNC = "rsync"
|
|
26
|
+
SCP = "scp"
|
|
27
|
+
SSH_CMD = "ssh_cmd"
|
|
28
|
+
HTTP_CHECK = "http_check"
|
|
29
|
+
VERSION_CHECK = "version_check"
|
|
30
|
+
WAIT = "wait"
|
|
31
|
+
PLUGIN = "plugin"
|
|
32
|
+
INLINE_SCRIPT = "inline_script"
|
|
33
|
+
ENSURE_CONFIG_LINE = "ensure_config_line"
|
|
34
|
+
RASPI_CONFIG = "raspi_config"
|
|
35
|
+
ENSURE_KANSHI_PROFILE = "ensure_kanshi_profile"
|
|
36
|
+
ENSURE_AUTOSTART_ENTRY = "ensure_autostart_entry"
|
|
37
|
+
ENSURE_BROWSER_KIOSK_SCRIPT = "ensure_browser_kiosk_script"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StepStatus(str, Enum):
|
|
41
|
+
PENDING = "pending"
|
|
42
|
+
RUNNING = "running"
|
|
43
|
+
DONE = "done"
|
|
44
|
+
FAILED = "failed"
|
|
45
|
+
SKIPPED = "skipped"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeployStrategy(str, Enum):
|
|
49
|
+
DOCKER_FULL = "docker_full"
|
|
50
|
+
NATIVE_KIOSK = "native_kiosk"
|
|
51
|
+
DOCKER_KIOSK = "docker_kiosk"
|
|
52
|
+
KIOSK_APPLIANCE = "kiosk_appliance"
|
|
53
|
+
PODMAN_QUADLET = "podman_quadlet"
|
|
54
|
+
K3S = "k3s"
|
|
55
|
+
SYSTEMD = "systemd"
|
|
56
|
+
UNKNOWN = "unknown"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# doql / external tool aliases → canonical DeployStrategy values
|
|
60
|
+
_STRATEGY_ALIASES: dict[str, str] = {
|
|
61
|
+
"docker-compose": "docker_full",
|
|
62
|
+
"quadlet": "podman_quadlet",
|
|
63
|
+
"kiosk-appliance": "kiosk_appliance",
|
|
64
|
+
"kiosk_appliance": "kiosk_appliance",
|
|
65
|
+
"kubernetes": "k3s",
|
|
66
|
+
"k8s": "k3s",
|
|
67
|
+
"native-kiosk": "native_kiosk",
|
|
68
|
+
"docker-kiosk": "docker_kiosk",
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""PersistedModel mixin — YAML serialization for BaseModel subclasses."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PersistedModel(BaseModel):
|
|
12
|
+
"""Mixin for models that can be persisted to/from YAML files."""
|
|
13
|
+
|
|
14
|
+
def to_yaml(self) -> str:
|
|
15
|
+
"""Serialize model to YAML string."""
|
|
16
|
+
return yaml.dump(
|
|
17
|
+
self.model_dump(mode="json"),
|
|
18
|
+
allow_unicode=True,
|
|
19
|
+
sort_keys=False,
|
|
20
|
+
default_flow_style=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def load(cls, path: Path) -> Self:
|
|
25
|
+
"""Load model from YAML file."""
|
|
26
|
+
raw = yaml.safe_load(path.read_text())
|
|
27
|
+
return cls(**raw)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from redeploy.heal.decider import Action, Decision, decide_after_failure, format_decision_message
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_decide_abort_on_loop_detected():
|
|
5
|
+
decision = decide_after_failure(
|
|
6
|
+
attempt=1,
|
|
7
|
+
max_retries=3,
|
|
8
|
+
failed_step="sync_env",
|
|
9
|
+
loop_detected=True,
|
|
10
|
+
llm_error=False,
|
|
11
|
+
spec_patched=False,
|
|
12
|
+
)
|
|
13
|
+
assert decision == Decision(Action.ABORT, "Loop detected: same hint for 'sync_env' ≥ 3 times in a row")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_decide_abort_when_failed_step_unknown():
|
|
17
|
+
decision = decide_after_failure(
|
|
18
|
+
attempt=1,
|
|
19
|
+
max_retries=3,
|
|
20
|
+
failed_step=None,
|
|
21
|
+
loop_detected=False,
|
|
22
|
+
llm_error=False,
|
|
23
|
+
spec_patched=False,
|
|
24
|
+
)
|
|
25
|
+
assert decision == Decision(Action.ABORT, "Cannot identify failed step — giving up")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_decide_retry_on_transient_llm_error_before_limit():
|
|
29
|
+
decision = decide_after_failure(
|
|
30
|
+
attempt=1,
|
|
31
|
+
max_retries=3,
|
|
32
|
+
failed_step="sync_env",
|
|
33
|
+
loop_detected=False,
|
|
34
|
+
llm_error=True,
|
|
35
|
+
spec_patched=False,
|
|
36
|
+
)
|
|
37
|
+
assert decision == Decision(Action.RETRY, "LLM error — retrying may help if transient")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_decide_abort_on_llm_error_at_retry_limit():
|
|
41
|
+
decision = decide_after_failure(
|
|
42
|
+
attempt=3,
|
|
43
|
+
max_retries=3,
|
|
44
|
+
failed_step="sync_env",
|
|
45
|
+
loop_detected=False,
|
|
46
|
+
llm_error=True,
|
|
47
|
+
spec_patched=False,
|
|
48
|
+
)
|
|
49
|
+
assert decision == Decision(Action.ABORT, "LLM error — retrying may help if transient")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_decide_abort_when_retry_budget_exhausted():
|
|
53
|
+
decision = decide_after_failure(
|
|
54
|
+
attempt=3,
|
|
55
|
+
max_retries=3,
|
|
56
|
+
failed_step="sync_env",
|
|
57
|
+
loop_detected=False,
|
|
58
|
+
llm_error=False,
|
|
59
|
+
spec_patched=False,
|
|
60
|
+
)
|
|
61
|
+
assert decision == Decision(Action.ABORT, "Max retries (3) exhausted for 'sync_env'")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_decide_retry_when_spec_was_patched():
|
|
65
|
+
decision = decide_after_failure(
|
|
66
|
+
attempt=1,
|
|
67
|
+
max_retries=3,
|
|
68
|
+
failed_step="sync_env",
|
|
69
|
+
loop_detected=False,
|
|
70
|
+
llm_error=False,
|
|
71
|
+
spec_patched=True,
|
|
72
|
+
)
|
|
73
|
+
assert decision == Decision(Action.RETRY, "Spec patched — re-running migration")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_decide_skip_when_patch_not_applicable():
|
|
77
|
+
decision = decide_after_failure(
|
|
78
|
+
attempt=1,
|
|
79
|
+
max_retries=3,
|
|
80
|
+
failed_step="sync_env",
|
|
81
|
+
loop_detected=False,
|
|
82
|
+
llm_error=False,
|
|
83
|
+
spec_patched=False,
|
|
84
|
+
)
|
|
85
|
+
assert decision == Decision(Action.SKIP, "LLM did not produce an applicable patch — skipping step")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_format_decision_message_uses_action_and_reason():
|
|
89
|
+
msg = format_decision_message(Decision(Action.RETRY, "Spec patched"), "sync_env")
|
|
90
|
+
assert "RETRY" in msg
|
|
91
|
+
assert "Spec patched" in msg
|