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.
Files changed (226) hide show
  1. {redeploy-0.2.55 → redeploy-0.2.57}/PKG-INFO +6 -6
  2. {redeploy-0.2.55 → redeploy-0.2.57}/README.md +5 -5
  3. {redeploy-0.2.55 → redeploy-0.2.57}/pyproject.toml +1 -1
  4. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/docker_compose.py +74 -49
  6. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/parsers/compose.py +30 -18
  7. redeploy-0.2.57/redeploy/models/enums.py +69 -0
  8. redeploy-0.2.57/redeploy/models/persisted.py +27 -0
  9. redeploy-0.2.57/redeploy/tests/test_heal_decider.py +91 -0
  10. redeploy-0.2.57/redeploy/tests/test_iac_compose_contract.py +181 -0
  11. redeploy-0.2.57/redeploy/tests/test_iac_docker_compose_contract.py +172 -0
  12. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/PKG-INFO +6 -6
  13. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/SOURCES.txt +5 -0
  14. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/__init__.py +0 -0
  15. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/exceptions.py +0 -0
  16. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/executor.py +0 -0
  17. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/handlers.py +0 -0
  18. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/progress.py +0 -0
  19. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/rollback.py +0 -0
  20. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/state.py +0 -0
  21. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/state_apply.py +0 -0
  22. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/utils/__init__.py +0 -0
  23. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/apply/utils/run_container_build.py +0 -0
  24. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/audit.py +0 -0
  25. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/__init__.py +0 -0
  26. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/extractor.py +0 -0
  27. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/generators/__init__.py +0 -0
  28. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/generators/docker_compose.py +0 -0
  29. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/generators/migration.py +0 -0
  30. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/__init__.py +0 -0
  31. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/compose.py +0 -0
  32. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/hardware.py +0 -0
  33. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/infra.py +0 -0
  34. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/blueprint/sources/migration.py +0 -0
  35. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/__init__.py +0 -0
  36. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/__init__.py +0 -0
  37. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/audit.py +0 -0
  38. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/blueprint.py +0 -0
  39. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/bump_fix.py +0 -0
  40. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/detect.py +0 -0
  41. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/device_map.py +0 -0
  42. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/device_map_renderers.py +0 -0
  43. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/devices.py +0 -0
  44. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/diagnose.py +0 -0
  45. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/diff.py +0 -0
  46. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/exec_.py +0 -0
  47. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/export.py +0 -0
  48. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/hardware.py +0 -0
  49. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/import_.py +0 -0
  50. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/init.py +0 -0
  51. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/inspect.py +0 -0
  52. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/mcp_cmd.py +0 -0
  53. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/patterns.py +0 -0
  54. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/plan_apply.py +0 -0
  55. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/plugin.py +0 -0
  56. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/probe.py +0 -0
  57. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/prompt_cmd.py +0 -0
  58. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/push.py +0 -0
  59. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/state.py +0 -0
  60. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/status.py +0 -0
  61. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/target.py +0 -0
  62. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/__init__.py +0 -0
  63. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/commands.py +0 -0
  64. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/helpers.py +0 -0
  65. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/monorepo.py +0 -0
  66. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/release.py +0 -0
  67. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/version/scanner.py +0 -0
  68. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/commands/workflow.py +0 -0
  69. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/core.py +0 -0
  70. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/display.py +0 -0
  71. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli/query.py +0 -0
  72. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/cli.py +0 -0
  73. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/config_apply/__init__.py +0 -0
  74. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/config_apply/applier.py +0 -0
  75. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/config_apply/loader.py +0 -0
  76. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/data_sync.py +0 -0
  77. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/__init__.py +0 -0
  78. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/builtin/__init__.py +0 -0
  79. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/detector.py +0 -0
  80. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/hardware.py +0 -0
  81. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/hardware_rules.py +0 -0
  82. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/probes.py +0 -0
  83. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/remote.py +0 -0
  84. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/templates.py +0 -0
  85. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/detect/workflow.py +0 -0
  86. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/discovery.py +0 -0
  87. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl/__init__.py +0 -0
  88. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl/loader.py +0 -0
  89. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl/parser.py +0 -0
  90. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/__init__.py +0 -0
  91. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/context.py +0 -0
  92. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/decorators.py +0 -0
  93. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/docker_steps.py +0 -0
  94. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/exceptions.py +0 -0
  95. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/runner.py +0 -0
  96. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/dsl_python/steps.py +0 -0
  97. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/fleet.py +0 -0
  98. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/__init__.py +0 -0
  99. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/config_txt.py +0 -0
  100. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/__init__.py +0 -0
  101. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/hyperpixel.py +0 -0
  102. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/official.py +0 -0
  103. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/data/waveshare.py +0 -0
  104. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/fixes.py +0 -0
  105. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/__init__.py +0 -0
  106. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/autostart.py +0 -0
  107. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/browsers.py +0 -0
  108. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/compositors.py +0 -0
  109. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  110. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/panels.py +0 -0
  111. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/hardware/raspi_config.py +0 -0
  112. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/__init__.py +0 -0
  113. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/decider.py +0 -0
  114. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/hint_provider.py +0 -0
  115. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/log_writer.py +0 -0
  116. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/loop_detector.py +0 -0
  117. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal/runner.py +0 -0
  118. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/heal.py +0 -0
  119. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/__init__.py +0 -0
  120. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/base.py +0 -0
  121. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/config_hints.py +0 -0
  122. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/parsers/__init__.py +0 -0
  123. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/iac/registry.py +0 -0
  124. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/integrations/__init__.py +0 -0
  125. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/integrations/op3_bridge.py +0 -0
  126. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/__init__.py +0 -0
  127. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/compiler.py +0 -0
  128. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/models.py +0 -0
  129. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/markpact/parser.py +0 -0
  130. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/mcp_server.py +0 -0
  131. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/models.py +0 -0
  132. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/observe.py +0 -0
  133. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/parse.py +0 -0
  134. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/patterns.py +0 -0
  135. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plan/__init__.py +0 -0
  136. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plan/planner.py +0 -0
  137. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/__init__.py +0 -0
  138. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/__init__.py +0 -0
  139. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/browser_reload.py +0 -0
  140. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  141. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/notify.py +0 -0
  142. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/process_control.py +0 -0
  143. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  144. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/schema.py +0 -0
  145. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/spec_loader.py +0 -0
  146. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/ssh.py +0 -0
  147. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/steps/__init__.py +0 -0
  148. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/steps/builtins.py +0 -0
  149. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/steps/kiosk.py +0 -0
  150. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/__init__.py +0 -0
  151. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_audit.py +0 -0
  152. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_cli.py +0 -0
  153. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_data_sync.py +0 -0
  154. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_discovery.py +0 -0
  155. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_dsl.py +0 -0
  156. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_examples.py +0 -0
  157. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_executor.py +0 -0
  158. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_fleet.py +0 -0
  159. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_git_integration.py +0 -0
  160. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_heal.py +0 -0
  161. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_hooks.py +0 -0
  162. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_iac.py +0 -0
  163. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_manifest.py +0 -0
  164. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_markpact_compiler.py +0 -0
  165. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_markpact_parser.py +0 -0
  166. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_models.py +0 -0
  167. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_observe.py +0 -0
  168. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_parse.py +0 -0
  169. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_patterns.py +0 -0
  170. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_planner_edge.py +0 -0
  171. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_plugins.py +0 -0
  172. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_probes.py +0 -0
  173. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_public_api.py +0 -0
  174. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_resume.py +0 -0
  175. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_spec_loader.py +0 -0
  176. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_ssh.py +0 -0
  177. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_templates.py +0 -0
  178. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_verify.py +0 -0
  179. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version.py +0 -0
  180. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_changelog.py +0 -0
  181. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_cli.py +0 -0
  182. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_manifest.py +0 -0
  183. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_version_sources.py +0 -0
  184. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/tests/test_workflow.py +0 -0
  185. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/verify.py +0 -0
  186. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/__init__.py +0 -0
  187. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/bump.py +0 -0
  188. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/changelog.py +0 -0
  189. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/commits.py +0 -0
  190. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/diff.py +0 -0
  191. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/git_integration.py +0 -0
  192. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/git_transaction.py +0 -0
  193. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/manifest.py +0 -0
  194. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/__init__.py +0 -0
  195. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/base.py +0 -0
  196. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/json_.py +0 -0
  197. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/plain.py +0 -0
  198. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/regex.py +0 -0
  199. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/toml_.py +0 -0
  200. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/sources/yaml_.py +0 -0
  201. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version/transaction.py +0 -0
  202. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy/version.py +0 -0
  203. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/dependency_links.txt +0 -0
  204. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/entry_points.txt +0 -0
  205. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/requires.txt +0 -0
  206. {redeploy-0.2.55 → redeploy-0.2.57}/redeploy.egg-info/top_level.txt +0 -0
  207. {redeploy-0.2.55 → redeploy-0.2.57}/setup.cfg +0 -0
  208. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_blueprint_sources.py +0 -0
  209. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_blueprint_sources_compose.py +0 -0
  210. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_cli_query.py +0 -0
  211. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_example_argocd_flux_plugin.py +0 -0
  212. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_example_gitops_ci_plugin.py +0 -0
  213. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_example_helm_kustomize_plugin.py +0 -0
  214. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_hardware_config.py +0 -0
  215. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_hardware_kiosk.py +0 -0
  216. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_hardware_rules.py +0 -0
  217. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_heal_decider.py +0 -0
  218. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_heal_loop_detector.py +0 -0
  219. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_iac.py +0 -0
  220. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_iac_config_hints.py +0 -0
  221. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_iac_plugin_loading.py +0 -0
  222. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_import_plugin_template.py +0 -0
  223. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_parse.py +0 -0
  224. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_prompt_llm.py +0 -0
  225. {redeploy-0.2.55 → redeploy-0.2.57}/tests/test_redeploy.py +0 -0
  226. {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.55
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.55-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.57-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
37
 
38
- - 🤖 **LLM usage:** $7.5000 (75 commits)
39
- - 👤 **Human dev:** ~$2853 (28.5h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.55-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.57-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.55-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.57-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-28.9h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (75 commits)
10
- - 👤 **Human dev:** ~$2853 (28.5h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.55-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.57-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.55"
7
+ version = "0.2.57"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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.55"
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 _parse_service(self, name: str, cfg: dict,
295
- env_context: dict[str, str],
296
- spec: ParsedSpec) -> ServiceInfo:
297
- def resolve(v: object) -> str:
298
- return _resolve_var(str(v), env_context) if v is not None else ""
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
- # ports
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
- severity="warn")
326
+ spec.add_warning(f"[{name}] Could not parse port: {raw!r}", severity="warn")
327
+ return ports
323
328
 
324
- # volumes
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
- # environment
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
- # resolve variables
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
- # env_file
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
- if isinstance(ef, str):
341
- env_files = [ef]
342
- elif isinstance(ef, list):
343
- env_files = [str(x) for x in ef]
344
-
345
- # networks
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
- svc_networks = list(svc_nets.keys())
349
- elif isinstance(svc_nets, list):
350
- svc_networks = [str(n) for n in svc_nets]
351
- else:
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
- # depends_on
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
- depends_on = [str(d) for d in dep]
358
- elif isinstance(dep, dict):
359
- depends_on = list(dep.keys())
360
- else:
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
- # healthcheck
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
- healthcheck = str(test) if test else None
377
+ return str(test) if test else None
378
+ return None
371
379
 
372
- # deploy.replicas
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
- replicas = int(deploy.get("replicas", 1))
383
+ return int(deploy.get("replicas", 1))
384
+ return 1
377
385
 
378
- # labels
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
- labels = {str(k): str(v) for k, v in labels_raw.items()}
382
- elif isinstance(labels_raw, list):
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
- else:
389
- labels = {}
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=str(cfg["command"]) if cfg.get("command") else None,
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, vol_def in (raw.get("volumes") or {}).items():
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
- if svc.image:
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 = int((d.get("deploy") or {}).get("replicas", 1))
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 = list(d.get("networks") or [])
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
- for secret in (d.get("secrets") or []):
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
- for ef in (d.get("env_file") or []):
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