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.
- {tweek-0.4.0/tweek.egg-info → tweek-0.4.2}/PKG-INFO +1 -1
- {tweek-0.4.0 → tweek-0.4.2}/pyproject.toml +1 -1
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_break_glass.py +65 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_cli.py +4 -3
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_config_models.py +14 -3
- tweek-0.4.2/tests/test_heuristic_chaining.py +106 -0
- tweek-0.4.2/tests/test_install_flow.py +150 -0
- tweek-0.4.2/tests/test_install_resilience.py +360 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_llm_reviewer.py +89 -0
- tweek-0.4.2/tests/test_llm_reviewer_lazy.py +73 -0
- tweek-0.4.2/tests/test_local_model_escalation.py +123 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_memory_scoped.py +87 -37
- tweek-0.4.2/tests/test_model_integrity.py +128 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_pattern_families.py +1 -1
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_pattern_matcher_redos.py +74 -56
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_patterns.py +152 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_post_tool_use.py +3 -3
- {tweek-0.4.0 → tweek-0.4.2}/tweek/__init__.py +1 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_core.py +23 -6
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_install.py +361 -91
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_uninstall.py +119 -36
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/families.yaml +13 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/models.py +31 -3
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/patterns.yaml +126 -2
- {tweek-0.4.0 → tweek-0.4.2}/tweek/diagnostics.py +124 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/break_glass.py +70 -47
- {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/overrides.py +19 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/post_tool_use.py +6 -2
- {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/pre_tool_use.py +19 -2
- tweek-0.4.2/tweek/hooks/wrapper_post_tool_use.py +121 -0
- tweek-0.4.2/tweek/hooks/wrapper_pre_tool_use.py +121 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/integrations/openclaw.py +70 -60
- tweek-0.4.2/tweek/integrations/openclaw_detection.py +140 -0
- tweek-0.4.2/tweek/integrations/openclaw_server.py +658 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/security_log.py +22 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/safety.py +7 -3
- {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/store.py +31 -10
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/base.py +9 -1
- tweek-0.4.2/tweek/plugins/detectors/openclaw.py +147 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/heuristic_scorer.py +12 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/local_model_reviewer.py +9 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/language.py +2 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/llm_reviewer.py +53 -24
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/local_model.py +21 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/rate_limiter.py +99 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/guard.py +30 -7
- {tweek-0.4.0 → tweek-0.4.2/tweek.egg-info}/PKG-INFO +1 -1
- {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/SOURCES.txt +9 -0
- tweek-0.4.0/tweek/integrations/openclaw_server.py +0 -385
- tweek-0.4.0/tweek/plugins/detectors/openclaw.py +0 -208
- {tweek-0.4.0 → tweek-0.4.2}/LICENSE +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/NOTICE +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/README.md +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/setup.cfg +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_approval_queue.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_audit.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_cli_configure.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_cli_helpers.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_config_manager.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_config_templates.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_credential_scanner.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_diagnostics.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_enforcement.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_feedback.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_heuristic_scorer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_installer_improvements.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_language_detection.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_licensing.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_llm_local.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_local_model.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_log_bundle.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_logging.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_logging_enhanced.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_mcp_clients.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_mcp_proxy.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_openclaw_integration.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_overrides.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_path_boundary.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_plugin_scoping.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_prompt_injection_patterns.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_property_based.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_protect_command.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_provenance.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_provenance_integration.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_proxy_detection.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_rate_limiter.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_redaction.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_screening_context.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_session_analyzer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_skill_context.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_tiered_help.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tests/test_vault_cross_platform.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/_keygen.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/audit.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_config.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_configure.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_dry_run.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_helpers.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_logs.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_mcp.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_memory.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_model.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_plugins.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_protect.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_proxy.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_security.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_skills.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/cli_vault.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/allowed_dirs.yaml +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/manager.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/config.yaml.template +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/env.template +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/overrides.yaml.template +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates/tweek.yaml.template +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/templates.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/config/tiers.yaml +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/hooks/feedback.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/integrations/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/licensing.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/bundle.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/logging/json_logger.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/approval.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/approval_cli.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/chatgpt.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/claude_desktop.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/clients/gemini.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/proxy.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/mcp/screening.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/provenance.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/queries.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/memory/schemas.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/platform/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/gdpr.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/gov.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/hipaa.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/legal.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/pci.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/compliance/soc2.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/continue_dev.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/copilot.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/cursor.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/detectors/windsurf.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_discovery.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_installer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_lockfile.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_registry.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/git_security.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/anthropic.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/azure_openai.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/bedrock.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/google.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/providers/openai.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/scope.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/llm_reviewer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/pattern_matcher.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/rate_limiter.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/plugins/screening/session_analyzer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/addon.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/interceptor.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/proxy/server.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/docker_bridge.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/executor.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/layers.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/linux.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/profile_generator.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/project.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/sandbox/registry.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/screening/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/screening/context.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/integrity.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/local_reviewer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/model_registry.py +2 -2
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/secret_scanner.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/security/session_analyzer.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/SKILL.md +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/cli-reference.md +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/overrides-reference.md +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/scripts/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skill_template/scripts/check_installed.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/config.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/context.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/fingerprints.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/isolation.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/skills/scanner.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/vault/__init__.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/vault/cross_platform.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek/vault/keychain.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek-openclaw-plugin/node_modules/flatted/python/flatted.py +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/dependency_links.txt +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/entry_points.txt +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/requires.txt +0 -0
- {tweek-0.4.0 → tweek-0.4.2}/tweek.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tweek"
|
|
7
|
-
version = "0.4.
|
|
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
|
|
116
|
-
|
|
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
|
|
615
|
-
|
|
616
|
-
|
|
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()
|