tweek 0.4.0__tar.gz → 0.4.2__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 (209) hide show
  1. {tweek-0.4.0/tweek.egg-info → tweek-0.4.2}/PKG-INFO +1 -1
  2. {tweek-0.4.0 → tweek-0.4.2}/pyproject.toml +1 -1
  3. {tweek-0.4.0 → tweek-0.4.2}/tests/test_break_glass.py +65 -0
  4. {tweek-0.4.0 → tweek-0.4.2}/tests/test_cli.py +4 -3
  5. {tweek-0.4.0 → tweek-0.4.2}/tests/test_config_models.py +14 -3
  6. tweek-0.4.2/tests/test_heuristic_chaining.py +106 -0
  7. tweek-0.4.2/tests/test_install_flow.py +150 -0
  8. tweek-0.4.2/tests/test_install_resilience.py +360 -0
  9. {tweek-0.4.0 → tweek-0.4.2}/tests/test_llm_reviewer.py +89 -0
  10. tweek-0.4.2/tests/test_llm_reviewer_lazy.py +73 -0
  11. tweek-0.4.2/tests/test_local_model_escalation.py +123 -0
  12. {tweek-0.4.0 → tweek-0.4.2}/tests/test_memory_scoped.py +87 -37
  13. tweek-0.4.2/tests/test_model_integrity.py +128 -0
  14. {tweek-0.4.0 → tweek-0.4.2}/tests/test_pattern_families.py +1 -1
  15. {tweek-0.4.0 → tweek-0.4.2}/tests/test_pattern_matcher_redos.py +74 -56
  16. {tweek-0.4.0 → tweek-0.4.2}/tests/test_patterns.py +152 -0
  17. {tweek-0.4.0 → tweek-0.4.2}/tests/test_post_tool_use.py +3 -3
  18. {tweek-0.4.0 → tweek-0.4.2}/tweek/__init__.py +1 -1
  19. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_core.py +23 -6
  20. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_install.py +361 -91
  21. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_uninstall.py +119 -36
  22. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/families.yaml +13 -0
  23. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/models.py +31 -3
  24. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/patterns.yaml +126 -2
  25. {tweek-0.4.0 → tweek-0.4.2}/tweek/diagnostics.py +124 -1
  26. {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/break_glass.py +70 -47
  27. {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/overrides.py +19 -1
  28. {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/post_tool_use.py +6 -2
  29. {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/pre_tool_use.py +19 -2
  30. tweek-0.4.2/tweek/hooks/wrapper_post_tool_use.py +121 -0
  31. tweek-0.4.2/tweek/hooks/wrapper_pre_tool_use.py +121 -0
  32. {tweek-0.4.0 → tweek-0.4.2}/tweek/integrations/openclaw.py +70 -60
  33. tweek-0.4.2/tweek/integrations/openclaw_detection.py +140 -0
  34. tweek-0.4.2/tweek/integrations/openclaw_server.py +658 -0
  35. {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/security_log.py +22 -0
  36. {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/safety.py +7 -3
  37. {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/store.py +31 -10
  38. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/base.py +9 -1
  39. tweek-0.4.2/tweek/plugins/detectors/openclaw.py +147 -0
  40. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/heuristic_scorer.py +12 -1
  41. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/local_model_reviewer.py +9 -0
  42. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/language.py +2 -1
  43. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/llm_reviewer.py +53 -24
  44. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/local_model.py +21 -0
  45. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/rate_limiter.py +99 -1
  46. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/guard.py +30 -7
  47. {tweek-0.4.0 → tweek-0.4.2/tweek.egg-info}/PKG-INFO +1 -1
  48. {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/SOURCES.txt +9 -0
  49. tweek-0.4.0/tweek/integrations/openclaw_server.py +0 -385
  50. tweek-0.4.0/tweek/plugins/detectors/openclaw.py +0 -208
  51. {tweek-0.4.0 → tweek-0.4.2}/LICENSE +0 -0
  52. {tweek-0.4.0 → tweek-0.4.2}/NOTICE +0 -0
  53. {tweek-0.4.0 → tweek-0.4.2}/README.md +0 -0
  54. {tweek-0.4.0 → tweek-0.4.2}/setup.cfg +0 -0
  55. {tweek-0.4.0 → tweek-0.4.2}/tests/test_approval_queue.py +0 -0
  56. {tweek-0.4.0 → tweek-0.4.2}/tests/test_audit.py +0 -0
  57. {tweek-0.4.0 → tweek-0.4.2}/tests/test_cli_configure.py +0 -0
  58. {tweek-0.4.0 → tweek-0.4.2}/tests/test_cli_helpers.py +0 -0
  59. {tweek-0.4.0 → tweek-0.4.2}/tests/test_config_manager.py +0 -0
  60. {tweek-0.4.0 → tweek-0.4.2}/tests/test_config_templates.py +0 -0
  61. {tweek-0.4.0 → tweek-0.4.2}/tests/test_credential_scanner.py +0 -0
  62. {tweek-0.4.0 → tweek-0.4.2}/tests/test_diagnostics.py +0 -0
  63. {tweek-0.4.0 → tweek-0.4.2}/tests/test_enforcement.py +0 -0
  64. {tweek-0.4.0 → tweek-0.4.2}/tests/test_feedback.py +0 -0
  65. {tweek-0.4.0 → tweek-0.4.2}/tests/test_heuristic_scorer.py +0 -0
  66. {tweek-0.4.0 → tweek-0.4.2}/tests/test_installer_improvements.py +0 -0
  67. {tweek-0.4.0 → tweek-0.4.2}/tests/test_language_detection.py +0 -0
  68. {tweek-0.4.0 → tweek-0.4.2}/tests/test_licensing.py +0 -0
  69. {tweek-0.4.0 → tweek-0.4.2}/tests/test_llm_local.py +0 -0
  70. {tweek-0.4.0 → tweek-0.4.2}/tests/test_local_model.py +0 -0
  71. {tweek-0.4.0 → tweek-0.4.2}/tests/test_log_bundle.py +0 -0
  72. {tweek-0.4.0 → tweek-0.4.2}/tests/test_logging.py +0 -0
  73. {tweek-0.4.0 → tweek-0.4.2}/tests/test_logging_enhanced.py +0 -0
  74. {tweek-0.4.0 → tweek-0.4.2}/tests/test_mcp_clients.py +0 -0
  75. {tweek-0.4.0 → tweek-0.4.2}/tests/test_mcp_proxy.py +0 -0
  76. {tweek-0.4.0 → tweek-0.4.2}/tests/test_openclaw_integration.py +0 -0
  77. {tweek-0.4.0 → tweek-0.4.2}/tests/test_overrides.py +0 -0
  78. {tweek-0.4.0 → tweek-0.4.2}/tests/test_path_boundary.py +0 -0
  79. {tweek-0.4.0 → tweek-0.4.2}/tests/test_plugin_scoping.py +0 -0
  80. {tweek-0.4.0 → tweek-0.4.2}/tests/test_prompt_injection_patterns.py +0 -0
  81. {tweek-0.4.0 → tweek-0.4.2}/tests/test_property_based.py +0 -0
  82. {tweek-0.4.0 → tweek-0.4.2}/tests/test_protect_command.py +0 -0
  83. {tweek-0.4.0 → tweek-0.4.2}/tests/test_provenance.py +0 -0
  84. {tweek-0.4.0 → tweek-0.4.2}/tests/test_provenance_integration.py +0 -0
  85. {tweek-0.4.0 → tweek-0.4.2}/tests/test_proxy_detection.py +0 -0
  86. {tweek-0.4.0 → tweek-0.4.2}/tests/test_rate_limiter.py +0 -0
  87. {tweek-0.4.0 → tweek-0.4.2}/tests/test_redaction.py +0 -0
  88. {tweek-0.4.0 → tweek-0.4.2}/tests/test_screening_context.py +0 -0
  89. {tweek-0.4.0 → tweek-0.4.2}/tests/test_session_analyzer.py +0 -0
  90. {tweek-0.4.0 → tweek-0.4.2}/tests/test_skill_context.py +0 -0
  91. {tweek-0.4.0 → tweek-0.4.2}/tests/test_tiered_help.py +0 -0
  92. {tweek-0.4.0 → tweek-0.4.2}/tests/test_vault_cross_platform.py +0 -0
  93. {tweek-0.4.0 → tweek-0.4.2}/tweek/_keygen.py +0 -0
  94. {tweek-0.4.0 → tweek-0.4.2}/tweek/audit.py +0 -0
  95. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli.py +0 -0
  96. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_config.py +0 -0
  97. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_configure.py +0 -0
  98. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_dry_run.py +0 -0
  99. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_helpers.py +0 -0
  100. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_logs.py +0 -0
  101. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_mcp.py +0 -0
  102. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_memory.py +0 -0
  103. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_model.py +0 -0
  104. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_plugins.py +0 -0
  105. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_protect.py +0 -0
  106. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_proxy.py +0 -0
  107. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_security.py +0 -0
  108. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_skills.py +0 -0
  109. {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_vault.py +0 -0
  110. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/__init__.py +0 -0
  111. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/allowed_dirs.yaml +0 -0
  112. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/manager.py +0 -0
  113. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/config.yaml.template +0 -0
  114. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/env.template +0 -0
  115. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/overrides.yaml.template +0 -0
  116. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/tweek.yaml.template +0 -0
  117. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates.py +0 -0
  118. {tweek-0.4.0 → tweek-0.4.2}/tweek/config/tiers.yaml +0 -0
  119. {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/__init__.py +0 -0
  120. {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/feedback.py +0 -0
  121. {tweek-0.4.0 → tweek-0.4.2}/tweek/integrations/__init__.py +0 -0
  122. {tweek-0.4.0 → tweek-0.4.2}/tweek/licensing.py +0 -0
  123. {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/__init__.py +0 -0
  124. {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/bundle.py +0 -0
  125. {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/json_logger.py +0 -0
  126. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/__init__.py +0 -0
  127. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/approval.py +0 -0
  128. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/approval_cli.py +0 -0
  129. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/__init__.py +0 -0
  130. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/chatgpt.py +0 -0
  131. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/claude_desktop.py +0 -0
  132. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/gemini.py +0 -0
  133. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/proxy.py +0 -0
  134. {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/screening.py +0 -0
  135. {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/__init__.py +0 -0
  136. {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/provenance.py +0 -0
  137. {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/queries.py +0 -0
  138. {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/schemas.py +0 -0
  139. {tweek-0.4.0 → tweek-0.4.2}/tweek/platform/__init__.py +0 -0
  140. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/__init__.py +0 -0
  141. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/__init__.py +0 -0
  142. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/gdpr.py +0 -0
  143. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/gov.py +0 -0
  144. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/hipaa.py +0 -0
  145. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/legal.py +0 -0
  146. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/pci.py +0 -0
  147. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/soc2.py +0 -0
  148. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/__init__.py +0 -0
  149. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/continue_dev.py +0 -0
  150. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/copilot.py +0 -0
  151. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/cursor.py +0 -0
  152. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/windsurf.py +0 -0
  153. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_discovery.py +0 -0
  154. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_installer.py +0 -0
  155. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_lockfile.py +0 -0
  156. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_registry.py +0 -0
  157. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_security.py +0 -0
  158. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/__init__.py +0 -0
  159. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/anthropic.py +0 -0
  160. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/azure_openai.py +0 -0
  161. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/bedrock.py +0 -0
  162. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/google.py +0 -0
  163. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/openai.py +0 -0
  164. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/scope.py +0 -0
  165. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/__init__.py +0 -0
  166. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/llm_reviewer.py +0 -0
  167. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/pattern_matcher.py +0 -0
  168. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/rate_limiter.py +0 -0
  169. {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/session_analyzer.py +0 -0
  170. {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/__init__.py +0 -0
  171. {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/addon.py +0 -0
  172. {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/interceptor.py +0 -0
  173. {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/server.py +0 -0
  174. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/__init__.py +0 -0
  175. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/docker_bridge.py +0 -0
  176. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/executor.py +0 -0
  177. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/layers.py +0 -0
  178. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/linux.py +0 -0
  179. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/profile_generator.py +0 -0
  180. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/project.py +0 -0
  181. {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/registry.py +0 -0
  182. {tweek-0.4.0 → tweek-0.4.2}/tweek/screening/__init__.py +0 -0
  183. {tweek-0.4.0 → tweek-0.4.2}/tweek/screening/context.py +0 -0
  184. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/__init__.py +0 -0
  185. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/integrity.py +0 -0
  186. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/local_reviewer.py +0 -0
  187. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/model_registry.py +2 -2
  188. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/secret_scanner.py +0 -0
  189. {tweek-0.4.0 → tweek-0.4.2}/tweek/security/session_analyzer.py +0 -0
  190. {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/SKILL.md +0 -0
  191. {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/__init__.py +0 -0
  192. {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/cli-reference.md +0 -0
  193. {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/overrides-reference.md +0 -0
  194. {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/scripts/__init__.py +0 -0
  195. {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/scripts/check_installed.py +0 -0
  196. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/__init__.py +0 -0
  197. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/config.py +0 -0
  198. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/context.py +0 -0
  199. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/fingerprints.py +0 -0
  200. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/isolation.py +0 -0
  201. {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/scanner.py +0 -0
  202. {tweek-0.4.0 → tweek-0.4.2}/tweek/vault/__init__.py +0 -0
  203. {tweek-0.4.0 → tweek-0.4.2}/tweek/vault/cross_platform.py +0 -0
  204. {tweek-0.4.0 → tweek-0.4.2}/tweek/vault/keychain.py +0 -0
  205. {tweek-0.4.0 → tweek-0.4.2}/tweek-openclaw-plugin/node_modules/flatted/python/flatted.py +0 -0
  206. {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/dependency_links.txt +0 -0
  207. {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/entry_points.txt +0 -0
  208. {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/requires.txt +0 -0
  209. {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tweek
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Defense-in-depth security for AI coding assistants - protect credentials, code, and system from prompt injection attacks
5
5
  Author: Tommy Mancino
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tweek"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Defense-in-depth security for AI coding assistants - protect credentials, code, and system from prompt injection attacks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -522,3 +522,68 @@ class TestExpiryBehavior:
522
522
 
523
523
  active = list_active_overrides()
524
524
  assert len(active) == 0
525
+
526
+
527
+ # =============================================================================
528
+ # FILE LOCKING TESTS (F4)
529
+ # =============================================================================
530
+
531
+ class TestFileLocking:
532
+ """Tests for fcntl.flock-based file locking in break-glass operations."""
533
+
534
+ def test_lock_file_created(self, isolate_state):
535
+ """Lock file should be created during break-glass operations."""
536
+ from tweek.hooks.break_glass import BREAK_GLASS_LOCK
537
+ create_override(pattern_name="lock_test", mode="once", reason="test")
538
+ assert BREAK_GLASS_LOCK.parent.exists()
539
+
540
+ def test_concurrent_single_use_override(self, isolate_state):
541
+ """Two threads consuming the same single-use override: exactly one should get it."""
542
+ import threading
543
+
544
+ create_override(pattern_name="concurrent_test", mode="once", reason="test")
545
+
546
+ results = []
547
+ errors = []
548
+
549
+ def check_in_thread():
550
+ try:
551
+ result = check_override("concurrent_test")
552
+ results.append(result)
553
+ except Exception as e:
554
+ errors.append(e)
555
+
556
+ t1 = threading.Thread(target=check_in_thread)
557
+ t2 = threading.Thread(target=check_in_thread)
558
+ t1.start()
559
+ t2.start()
560
+ t1.join()
561
+ t2.join()
562
+
563
+ assert len(errors) == 0
564
+ assert len(results) == 2
565
+ # Exactly one thread should get the override, the other gets None
566
+ non_none = [r for r in results if r is not None]
567
+ nones = [r for r in results if r is None]
568
+ assert len(non_none) == 1
569
+ assert len(nones) == 1
570
+ assert non_none[0]["pattern"] == "concurrent_test"
571
+
572
+ def test_existing_tests_still_pass_with_locking(self, isolate_state):
573
+ """Basic create/check/clear cycle works with file locking."""
574
+ override = create_override(
575
+ pattern_name="basic_lock_test", mode="once", reason="verify locking"
576
+ )
577
+ assert override["pattern"] == "basic_lock_test"
578
+
579
+ result = check_override("basic_lock_test")
580
+ assert result is not None
581
+ assert result["used"] is True
582
+
583
+ # Second check should return None (consumed)
584
+ result2 = check_override("basic_lock_test")
585
+ assert result2 is None
586
+
587
+ # Clear should work
588
+ count = clear_overrides()
589
+ assert count >= 1
@@ -103,7 +103,7 @@ class TestInstallCommand:
103
103
  assert "paranoid" in result.output.lower() or result.exit_code == 0
104
104
 
105
105
  def test_install_skip_proxy_check(self, runner, tmp_path):
106
- """Test protect claude-code with --skip-proxy-check skips openclaw detection."""
106
+ """Test protect claude-code with --skip-proxy-check skips openclaw proxy detection."""
107
107
  with patch.dict(os.environ, {'HOME': str(tmp_path)}):
108
108
  with patch.object(Path, 'home', return_value=tmp_path):
109
109
  with patch('tweek.cli_install.Path.home', return_value=tmp_path):
@@ -112,8 +112,9 @@ class TestInstallCommand:
112
112
  ['protect', 'claude-code', '--skip-env-scan', '--skip-proxy-check']
113
113
  )
114
114
 
115
- # Should not mention openclaw
116
- assert "openclaw" not in result.output.lower()
115
+ # Should not trigger openclaw proxy conflict detection (step 4)
116
+ # Note: openclaw may appear in the tool detection table (step 14) — that's expected
117
+ assert "proxy conflict" not in result.output.lower()
117
118
  assert result.exit_code == 0 or "Installation complete" in result.output
118
119
 
119
120
  def test_install_detects_openclaw_installed(self, runner, tmp_path):
@@ -611,9 +611,20 @@ class TestOpenClawConfig:
611
611
  assert cfg.plugin_installed is True
612
612
  assert cfg.preset == "paranoid"
613
613
 
614
- def test_extra_fields_allowed(self):
615
- cfg = OpenClawConfig(extra_key="value")
616
- assert cfg.extra_key == "value"
614
+ def test_extra_fields_rejected(self):
615
+ with pytest.raises(ValidationError):
616
+ OpenClawConfig(extra_key="value")
617
+
618
+ def test_preset_validation(self):
619
+ for valid in ("paranoid", "cautious", "balanced", "trusted"):
620
+ cfg = OpenClawConfig(preset=valid)
621
+ assert cfg.preset == valid
622
+ with pytest.raises(ValidationError):
623
+ OpenClawConfig(preset="invalid")
624
+
625
+ def test_port_collision_rejected(self):
626
+ with pytest.raises(ValidationError, match="must differ"):
627
+ OpenClawConfig(gateway_port=9000, scanner_port=9000)
617
628
 
618
629
 
619
630
  # =============================================================================
@@ -0,0 +1,106 @@
1
+ """Tests for heuristic scorer benign dampening chain detection (Finding F7).
2
+
3
+ Validates that command chaining operators (&&, ||, ;) prevent benign
4
+ dampening from being applied, since a benign prefix does not make the
5
+ entire chained command benign.
6
+ """
7
+
8
+ import pytest
9
+
10
+ from tweek.plugins.screening.heuristic_scorer import HeuristicScorerPlugin
11
+
12
+
13
+ @pytest.fixture
14
+ def scorer():
15
+ return HeuristicScorerPlugin()
16
+
17
+
18
+ class TestBenignDampeningChainDetection:
19
+ """Tests for _is_benign() rejecting chained commands."""
20
+
21
+ def test_simple_git_status_is_benign(self, scorer):
22
+ """A simple benign command (no chaining) should be detected as benign."""
23
+ result = scorer._is_benign("git status")
24
+ assert result is not None
25
+
26
+ def test_simple_pip_install_is_benign(self, scorer):
27
+ """pip install without chaining should be detected as benign."""
28
+ result = scorer._is_benign("pip install requests")
29
+ assert result is not None
30
+
31
+ def test_simple_make_is_benign(self, scorer):
32
+ """'make' without chaining should be benign."""
33
+ result = scorer._is_benign("make clean")
34
+ assert result is not None
35
+
36
+ def test_simple_ls_is_benign(self, scorer):
37
+ """'ls' without chaining should be benign."""
38
+ result = scorer._is_benign("ls -la")
39
+ assert result is not None
40
+
41
+ def test_and_and_chaining_not_benign(self, scorer):
42
+ """Benign prefix with && chaining should NOT be benign."""
43
+ result = scorer._is_benign("git commit && some_other_command")
44
+ assert result is None
45
+
46
+ def test_semicolon_chaining_not_benign(self, scorer):
47
+ """Benign prefix with ; chaining should NOT be benign."""
48
+ result = scorer._is_benign("pip install foo; some_other_command")
49
+ assert result is None
50
+
51
+ def test_or_chaining_not_benign(self, scorer):
52
+ """Benign prefix with || chaining should NOT be benign."""
53
+ result = scorer._is_benign("echo hello || some_other_command")
54
+ assert result is None
55
+
56
+ def test_make_with_chaining_not_benign(self, scorer):
57
+ """'make' with chaining should NOT be benign."""
58
+ result = scorer._is_benign("make clean && some_other_command")
59
+ assert result is None
60
+
61
+ def test_docker_with_chaining_not_benign(self, scorer):
62
+ """'docker build' with chaining should NOT be benign."""
63
+ result = scorer._is_benign("docker build . && some_other_command")
64
+ assert result is None
65
+
66
+ def test_non_matching_command_not_benign(self, scorer):
67
+ """A command that doesn't match any benign pattern returns None."""
68
+ result = scorer._is_benign("some_unknown_tool --flag")
69
+ assert result is None
70
+
71
+
72
+ class TestBenignDampeningScoreImpact:
73
+ """Tests that chaining detection affects the overall score correctly."""
74
+
75
+ def test_simple_benign_command_scores_zero(self, scorer):
76
+ """A simple benign command with no suspicious signals scores 0."""
77
+ result = scorer._score_content("git commit -m 'test message'")
78
+ assert result.total_score == 0.0
79
+
80
+ def test_chained_command_not_dampened(self, scorer):
81
+ """A chained command should not have dampening applied."""
82
+ # Use a command with chaining -- even if prefix is benign,
83
+ # dampening should be skipped
84
+ result = scorer._score_content("git status && some_other_command")
85
+ assert result.dampened is False
86
+
87
+ def test_unchained_benign_with_signals_is_dampened(self, scorer):
88
+ """An unchained benign command that happens to have signals gets dampened."""
89
+ # 'cat' on a regular code file is benign
90
+ result = scorer._score_content("cat test_file.py")
91
+ # Should be dampened if it matches a benign pattern
92
+ # Score is 0 anyway for benign content, so dampening is a no-op
93
+ # Just verify no crash
94
+ assert result.total_score >= 0.0
95
+
96
+ def test_pipe_only_is_still_benign(self, scorer):
97
+ """Pipe (|) alone should NOT prevent benign detection.
98
+
99
+ Pipes are different from command chaining (&&, ||, ;).
100
+ A pipe sends output to another command but is a single pipeline,
101
+ not multiple independent commands.
102
+ """
103
+ result = scorer._is_benign("git log | head -20")
104
+ # This should still be detected as benign (git log matches benign pattern)
105
+ # and the pipe is not a chaining operator
106
+ assert result is not None
@@ -0,0 +1,150 @@
1
+ """Tests for install flow improvements.
2
+
3
+ Covers: auto model deps, multi-tool detection, API key validation,
4
+ preset descriptions, and doctor --fix mode.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import pytest
10
+ from unittest.mock import patch, MagicMock
11
+ from pathlib import Path
12
+
13
+
14
+ @pytest.mark.cli
15
+ class TestAutoModelDeps:
16
+ """_ensure_local_model_deps installs missing packages."""
17
+
18
+ def test_deps_already_available(self):
19
+ """Returns True immediately when deps are importable."""
20
+ from tweek.cli_install import _ensure_local_model_deps
21
+
22
+ with patch("builtins.__import__", side_effect=lambda name, *a, **kw: MagicMock()):
23
+ result = _ensure_local_model_deps()
24
+ assert result is True
25
+
26
+ def test_deps_install_failure_returns_false(self):
27
+ """Returns False when pip install fails."""
28
+ from tweek.cli_install import _ensure_local_model_deps
29
+
30
+ # First import raises ImportError, subprocess fails
31
+ original_import = __builtins__.__import__ if hasattr(__builtins__, '__import__') else __import__
32
+
33
+ def mock_import(name, *args, **kwargs):
34
+ if name in ("onnxruntime", "tokenizers", "numpy"):
35
+ raise ImportError(f"No module named '{name}'")
36
+ return original_import(name, *args, **kwargs)
37
+
38
+ with patch("builtins.__import__", side_effect=mock_import):
39
+ with patch("subprocess.run") as mock_run:
40
+ mock_run.return_value = MagicMock(returncode=1, stderr="pip error")
41
+ result = _ensure_local_model_deps()
42
+ assert result is False
43
+
44
+
45
+ @pytest.mark.cli
46
+ class TestDetectLlmProvider:
47
+ """_detect_llm_provider checks both API key and SDK availability."""
48
+
49
+ def test_google_key_without_sdk_skipped(self):
50
+ """Google key present but SDK not importable returns None."""
51
+ from importlib import import_module as real_import_module
52
+ from tweek.cli_install import _detect_llm_provider
53
+
54
+ def selective_import(name, *args, **kwargs):
55
+ """Block only the SDK modules, let everything else through."""
56
+ if name in ("google.generativeai", "openai", "anthropic"):
57
+ raise ImportError(f"No module named '{name}'")
58
+ return real_import_module(name, *args, **kwargs)
59
+
60
+ # Ensure no other API keys leak through
61
+ env_clean = {
62
+ "GOOGLE_API_KEY": "test-key",
63
+ "OPENAI_API_KEY": "",
64
+ "XAI_API_KEY": "",
65
+ "ANTHROPIC_API_KEY": "",
66
+ "GEMINI_API_KEY": "",
67
+ }
68
+
69
+ with patch.dict(os.environ, env_clean, clear=False):
70
+ with patch("tweek.cli_install.LOCAL_MODEL_AVAILABLE", False, create=True):
71
+ with patch("importlib.import_module", side_effect=selective_import):
72
+ result = _detect_llm_provider()
73
+ assert result is None
74
+
75
+ def test_google_key_with_sdk_detected(self):
76
+ """Google key + SDK returns Google provider."""
77
+ from tweek.cli_install import _detect_llm_provider
78
+
79
+ with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}, clear=False):
80
+ with patch("importlib.import_module", return_value=MagicMock()):
81
+ with patch("tweek.security.local_model.LOCAL_MODEL_AVAILABLE", False):
82
+ result = _detect_llm_provider()
83
+ assert result is not None
84
+ assert result["name"] == "Google"
85
+
86
+ def test_local_model_takes_priority(self):
87
+ """Local model is preferred over cloud providers."""
88
+ from tweek.cli_install import _detect_llm_provider
89
+
90
+ with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}, clear=False):
91
+ with patch("tweek.security.local_model.LOCAL_MODEL_AVAILABLE", True):
92
+ with patch("tweek.security.model_registry.is_model_installed", return_value=True):
93
+ with patch("tweek.security.model_registry.get_default_model_name", return_value="deberta"):
94
+ result = _detect_llm_provider()
95
+ assert result is not None
96
+ assert result["name"] == "Local model"
97
+
98
+
99
+ @pytest.mark.cli
100
+ class TestDetectAndShowTools:
101
+ """_detect_and_show_tools returns unprotected tools."""
102
+
103
+ def test_returns_unprotected_only(self):
104
+ """Only installed+unprotected tools are returned."""
105
+ from tweek.cli_install import _detect_and_show_tools
106
+
107
+ mock_tools = [
108
+ ("claude-code", "Claude Code", True, True, ""),
109
+ ("gemini", "Gemini CLI", True, False, ""),
110
+ ("chatgpt", "ChatGPT Desktop", False, False, ""),
111
+ ]
112
+ with patch("tweek.cli_install._detect_all_tools", return_value=mock_tools):
113
+ result = _detect_and_show_tools()
114
+ assert len(result) == 1
115
+ assert result[0][0] == "gemini"
116
+
117
+ def test_returns_empty_when_all_protected(self):
118
+ """Returns empty list when all tools are already protected."""
119
+ from tweek.cli_install import _detect_and_show_tools
120
+
121
+ mock_tools = [
122
+ ("claude-code", "Claude Code", True, True, ""),
123
+ ]
124
+ with patch("tweek.cli_install._detect_all_tools", return_value=mock_tools):
125
+ result = _detect_and_show_tools()
126
+ assert len(result) == 0
127
+
128
+
129
+ @pytest.mark.core
130
+ class TestDoctorFixFlag:
131
+ """tweek doctor --fix triggers interactive mode."""
132
+
133
+ def test_run_health_checks_accepts_interactive(self):
134
+ """run_health_checks accepts interactive parameter."""
135
+ from tweek.diagnostics import run_health_checks
136
+
137
+ # Should not raise even with interactive=True
138
+ # (it just won't prompt since there may be no fixable issues)
139
+ with patch("tweek.diagnostics._offer_interactive_fixes") as mock_fix:
140
+ results = run_health_checks(verbose=False, interactive=True)
141
+ mock_fix.assert_called_once()
142
+ assert isinstance(results, list)
143
+
144
+ def test_non_interactive_skips_fixes(self):
145
+ """Default mode does not call _offer_interactive_fixes."""
146
+ from tweek.diagnostics import run_health_checks
147
+
148
+ with patch("tweek.diagnostics._offer_interactive_fixes") as mock_fix:
149
+ results = run_health_checks(verbose=False, interactive=False)
150
+ mock_fix.assert_not_called()