adversarial-workflow 0.6.0__tar.gz → 0.6.1__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 (52) hide show
  1. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/PKG-INFO +34 -4
  2. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/README.md +33 -3
  3. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/__init__.py +1 -1
  4. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/cli.py +29 -10
  5. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow.egg-info/PKG-INFO +34 -4
  6. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow.egg-info/SOURCES.txt +1 -0
  7. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/pyproject.toml +1 -1
  8. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_cli.py +1 -1
  9. adversarial_workflow-0.6.1/tests/test_env_loading.py +214 -0
  10. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/LICENSE +0 -0
  11. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/__main__.py +0 -0
  12. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/evaluators/__init__.py +0 -0
  13. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/evaluators/builtins.py +0 -0
  14. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/evaluators/config.py +0 -0
  15. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/evaluators/discovery.py +0 -0
  16. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/evaluators/runner.py +0 -0
  17. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/.aider.conf.yml.template +0 -0
  18. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/.env.example.template +0 -0
  19. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/README.template +0 -0
  20. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/agent-context/AGENT-SYSTEM-GUIDE.md +0 -0
  21. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/agent-context/README.md.template +0 -0
  22. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/agent-context/agent-handoffs-minimal.json.template +0 -0
  23. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/agent-context/agent-handoffs.json.template +0 -0
  24. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/agent-context/current-state.json.template +0 -0
  25. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/config.yml.template +0 -0
  26. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/evaluate_plan.sh.template +0 -0
  27. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/example-task.md.template +0 -0
  28. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/proofread_content.sh.template +0 -0
  29. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/review_implementation.sh.template +0 -0
  30. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/templates/validate_tests.sh.template +0 -0
  31. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/utils/__init__.py +0 -0
  32. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/utils/colors.py +0 -0
  33. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/utils/config.py +0 -0
  34. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/utils/file_splitter.py +0 -0
  35. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow/utils/validation.py +0 -0
  36. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow.egg-info/dependency_links.txt +0 -0
  37. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow.egg-info/entry_points.txt +0 -0
  38. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow.egg-info/requires.txt +0 -0
  39. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/adversarial_workflow.egg-info/top_level.txt +0 -0
  40. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/setup.cfg +0 -0
  41. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/setup.py +0 -0
  42. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_cli_dynamic_commands.py +0 -0
  43. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_config.py +0 -0
  44. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_evaluate.py +0 -0
  45. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_evaluator_config.py +0 -0
  46. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_evaluator_discovery.py +0 -0
  47. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_evaluator_runner.py +0 -0
  48. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_file_splitter.py +0 -0
  49. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_list_evaluators.py +0 -0
  50. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_python_version.py +0 -0
  51. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_split_command.py +0 -0
  52. {adversarial_workflow-0.6.0 → adversarial_workflow-0.6.1}/tests/test_utils_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adversarial-workflow
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern
5
5
  Author: Fredrik Matheson
6
6
  License: MIT
@@ -35,6 +35,10 @@ Dynamic: license-file
35
35
 
36
36
  # Adversarial Workflow
37
37
 
38
+ [![PyPI version](https://badge.fury.io/py/adversarial-workflow.svg)](https://pypi.org/project/adversarial-workflow/)
39
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
+
38
42
  **A multi-stage AI code review system that makes your code better**
39
43
 
40
44
  Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to implement but not delivering) through adversarial verification using independent review stages. A battle-tested workflow from the [thematic-cuts](https://github.com/movito/thematic-cuts) project that achieved 96.9% test pass rate improvement.
@@ -51,6 +55,31 @@ Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to i
51
55
  - 🎯 **Tool-agnostic**: Use with Claude Code, Cursor, Aider, manual coding, or any workflow
52
56
  - ✨ **Interactive onboarding**: Guided setup wizard gets you started in <5 minutes
53
57
 
58
+ ## What's New in v0.6.0
59
+
60
+ 🔌 **Plugin Architecture** - Define custom evaluators without modifying the package:
61
+
62
+ ```bash
63
+ # Create a custom evaluator
64
+ mkdir -p .adversarial/evaluators
65
+ cat > .adversarial/evaluators/athena.yml << 'EOF'
66
+ name: athena
67
+ description: Knowledge evaluation using Gemini 2.5 Pro
68
+ model: gemini-2.5-pro
69
+ api_key_env: GEMINI_API_KEY
70
+ prompt: |
71
+ You are Athena, a knowledge evaluation specialist...
72
+ EOF
73
+
74
+ # Use it immediately
75
+ adversarial athena docs/research-plan.md
76
+
77
+ # List all available evaluators
78
+ adversarial list-evaluators
79
+ ```
80
+
81
+ See [Custom Evaluators](#custom-evaluators) for full documentation, or check the [CHANGELOG](CHANGELOG.md) for complete release history.
82
+
54
83
  ## Prerequisites
55
84
 
56
85
  Before installing, ensure you have:
@@ -856,12 +885,13 @@ From the [thematic-cuts](https://github.com/movito/thematic-cuts) project:
856
885
 
857
886
  ## Documentation
858
887
 
859
- - **Interaction Patterns**: How Author-Reviewer collaboration works
888
+ - **[Custom Evaluators Guide](docs/CUSTOM_EVALUATORS.md)**: Create project-specific evaluators
889
+ - **[Integration Guide](docs/INTEGRATION-GUIDE.md)**: Detailed integration strategies
890
+ - **[CHANGELOG](CHANGELOG.md)**: Release history and version notes
891
+ - **Interaction Patterns**: How Author-Evaluator collaboration works
860
892
  - **Token Optimization**: Detailed Aider configuration guide
861
893
  - **Workflow Phases**: Step-by-step guide for each phase
862
894
  - **Troubleshooting**: Common issues and solutions
863
- - **Examples**: Real integration scenarios
864
- - **Terminology**: Official standards for Author/Reviewer concepts
865
895
 
866
896
  See `docs/` directory for comprehensive guides.
867
897
 
@@ -1,5 +1,9 @@
1
1
  # Adversarial Workflow
2
2
 
3
+ [![PyPI version](https://badge.fury.io/py/adversarial-workflow.svg)](https://pypi.org/project/adversarial-workflow/)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  **A multi-stage AI code review system that makes your code better**
4
8
 
5
9
  Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to implement but not delivering) through adversarial verification using independent review stages. A battle-tested workflow from the [thematic-cuts](https://github.com/movito/thematic-cuts) project that achieved 96.9% test pass rate improvement.
@@ -16,6 +20,31 @@ Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to i
16
20
  - 🎯 **Tool-agnostic**: Use with Claude Code, Cursor, Aider, manual coding, or any workflow
17
21
  - ✨ **Interactive onboarding**: Guided setup wizard gets you started in <5 minutes
18
22
 
23
+ ## What's New in v0.6.0
24
+
25
+ 🔌 **Plugin Architecture** - Define custom evaluators without modifying the package:
26
+
27
+ ```bash
28
+ # Create a custom evaluator
29
+ mkdir -p .adversarial/evaluators
30
+ cat > .adversarial/evaluators/athena.yml << 'EOF'
31
+ name: athena
32
+ description: Knowledge evaluation using Gemini 2.5 Pro
33
+ model: gemini-2.5-pro
34
+ api_key_env: GEMINI_API_KEY
35
+ prompt: |
36
+ You are Athena, a knowledge evaluation specialist...
37
+ EOF
38
+
39
+ # Use it immediately
40
+ adversarial athena docs/research-plan.md
41
+
42
+ # List all available evaluators
43
+ adversarial list-evaluators
44
+ ```
45
+
46
+ See [Custom Evaluators](#custom-evaluators) for full documentation, or check the [CHANGELOG](CHANGELOG.md) for complete release history.
47
+
19
48
  ## Prerequisites
20
49
 
21
50
  Before installing, ensure you have:
@@ -821,12 +850,13 @@ From the [thematic-cuts](https://github.com/movito/thematic-cuts) project:
821
850
 
822
851
  ## Documentation
823
852
 
824
- - **Interaction Patterns**: How Author-Reviewer collaboration works
853
+ - **[Custom Evaluators Guide](docs/CUSTOM_EVALUATORS.md)**: Create project-specific evaluators
854
+ - **[Integration Guide](docs/INTEGRATION-GUIDE.md)**: Detailed integration strategies
855
+ - **[CHANGELOG](CHANGELOG.md)**: Release history and version notes
856
+ - **Interaction Patterns**: How Author-Evaluator collaboration works
825
857
  - **Token Optimization**: Detailed Aider configuration guide
826
858
  - **Workflow Phases**: Step-by-step guide for each phase
827
859
  - **Troubleshooting**: Common issues and solutions
828
- - **Examples**: Real integration scenarios
829
- - **Terminology**: Official standards for Author/Reviewer concepts
830
860
 
831
861
  See `docs/` directory for comprehensive guides.
832
862
 
@@ -12,7 +12,7 @@ Usage:
12
12
  adversarial validate "pytest"
13
13
  """
14
14
 
15
- __version__ = "0.6.0"
15
+ __version__ = "0.6.1"
16
16
  __author__ = "Fredrik Matheson"
17
17
  __license__ = "MIT"
18
18
 
@@ -27,9 +27,9 @@ from pathlib import Path
27
27
  from typing import Dict, List, Optional, Tuple
28
28
 
29
29
  import yaml
30
- from dotenv import load_dotenv
30
+ from dotenv import load_dotenv, dotenv_values
31
31
 
32
- __version__ = "0.6.0"
32
+ __version__ = "0.6.1"
33
33
 
34
34
  # ANSI color codes for better output
35
35
  RESET = "\033[0m"
@@ -800,26 +800,37 @@ def check() -> int:
800
800
  issues: List[Dict] = []
801
801
  good_checks: List[str] = []
802
802
 
803
- # Check for .env file first (before loading environment variables)
803
+ # Check for .env file (note: already loaded by main() at startup)
804
804
  env_file = Path(".env")
805
805
  env_loaded = False
806
- env_keys_before = set(os.environ.keys())
807
806
 
808
807
  if env_file.exists():
809
808
  try:
809
+ # Load .env into environment (idempotent - safe to call again after main())
810
810
  load_dotenv(env_file)
811
- env_keys_after = set(os.environ.keys())
812
- new_keys = env_keys_after - env_keys_before
811
+ # Use dotenv_values() to count variables directly from file
812
+ # This gives accurate count regardless of what was already in environment
813
+ env_vars = dotenv_values(env_file)
813
814
  env_loaded = True
814
815
  good_checks.append(
815
- f".env file found and loaded ({len(new_keys)} variables)"
816
+ f".env file found ({len(env_vars)} variables configured)"
816
817
  )
817
- except Exception as e:
818
+ except (FileNotFoundError, PermissionError) as e:
819
+ # File access errors
818
820
  issues.append(
819
821
  {
820
822
  "severity": "WARNING",
821
- "message": f".env file found but could not be loaded: {e}",
822
- "fix": "Check .env file format and permissions",
823
+ "message": f".env file found but could not be read: {e}",
824
+ "fix": "Check .env file permissions",
825
+ }
826
+ )
827
+ except (OSError, ValueError) as e:
828
+ # Covers UnicodeDecodeError (ValueError subclass) and other OS errors
829
+ issues.append(
830
+ {
831
+ "severity": "WARNING",
832
+ "message": f".env file found but could not be parsed: {e}",
833
+ "fix": "Check .env file encoding (should be UTF-8)",
823
834
  }
824
835
  )
825
836
  else:
@@ -2868,6 +2879,14 @@ def list_evaluators() -> int:
2868
2879
  def main():
2869
2880
  """Main CLI entry point."""
2870
2881
  import logging
2882
+ import sys
2883
+
2884
+ # Load .env file before any commands run
2885
+ # Wrapped in try/except so CLI remains usable even with malformed .env
2886
+ try:
2887
+ load_dotenv()
2888
+ except Exception as e:
2889
+ print(f"Warning: Could not load .env file: {e}", file=sys.stderr)
2871
2890
 
2872
2891
  from adversarial_workflow.evaluators import (
2873
2892
  get_all_evaluators,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adversarial-workflow
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern
5
5
  Author: Fredrik Matheson
6
6
  License: MIT
@@ -35,6 +35,10 @@ Dynamic: license-file
35
35
 
36
36
  # Adversarial Workflow
37
37
 
38
+ [![PyPI version](https://badge.fury.io/py/adversarial-workflow.svg)](https://pypi.org/project/adversarial-workflow/)
39
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
+
38
42
  **A multi-stage AI code review system that makes your code better**
39
43
 
40
44
  Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to implement but not delivering) through adversarial verification using independent review stages. A battle-tested workflow from the [thematic-cuts](https://github.com/movito/thematic-cuts) project that achieved 96.9% test pass rate improvement.
@@ -51,6 +55,31 @@ Evaluate proposals, sort out ideas, and prevent "phantom work" (AI claiming to i
51
55
  - 🎯 **Tool-agnostic**: Use with Claude Code, Cursor, Aider, manual coding, or any workflow
52
56
  - ✨ **Interactive onboarding**: Guided setup wizard gets you started in <5 minutes
53
57
 
58
+ ## What's New in v0.6.0
59
+
60
+ 🔌 **Plugin Architecture** - Define custom evaluators without modifying the package:
61
+
62
+ ```bash
63
+ # Create a custom evaluator
64
+ mkdir -p .adversarial/evaluators
65
+ cat > .adversarial/evaluators/athena.yml << 'EOF'
66
+ name: athena
67
+ description: Knowledge evaluation using Gemini 2.5 Pro
68
+ model: gemini-2.5-pro
69
+ api_key_env: GEMINI_API_KEY
70
+ prompt: |
71
+ You are Athena, a knowledge evaluation specialist...
72
+ EOF
73
+
74
+ # Use it immediately
75
+ adversarial athena docs/research-plan.md
76
+
77
+ # List all available evaluators
78
+ adversarial list-evaluators
79
+ ```
80
+
81
+ See [Custom Evaluators](#custom-evaluators) for full documentation, or check the [CHANGELOG](CHANGELOG.md) for complete release history.
82
+
54
83
  ## Prerequisites
55
84
 
56
85
  Before installing, ensure you have:
@@ -856,12 +885,13 @@ From the [thematic-cuts](https://github.com/movito/thematic-cuts) project:
856
885
 
857
886
  ## Documentation
858
887
 
859
- - **Interaction Patterns**: How Author-Reviewer collaboration works
888
+ - **[Custom Evaluators Guide](docs/CUSTOM_EVALUATORS.md)**: Create project-specific evaluators
889
+ - **[Integration Guide](docs/INTEGRATION-GUIDE.md)**: Detailed integration strategies
890
+ - **[CHANGELOG](CHANGELOG.md)**: Release history and version notes
891
+ - **Interaction Patterns**: How Author-Evaluator collaboration works
860
892
  - **Token Optimization**: Detailed Aider configuration guide
861
893
  - **Workflow Phases**: Step-by-step guide for each phase
862
894
  - **Troubleshooting**: Common issues and solutions
863
- - **Examples**: Real integration scenarios
864
- - **Terminology**: Official standards for Author/Reviewer concepts
865
895
 
866
896
  See `docs/` directory for comprehensive guides.
867
897
 
@@ -38,6 +38,7 @@ adversarial_workflow/utils/validation.py
38
38
  tests/test_cli.py
39
39
  tests/test_cli_dynamic_commands.py
40
40
  tests/test_config.py
41
+ tests/test_env_loading.py
41
42
  tests/test_evaluate.py
42
43
  tests/test_evaluator_config.py
43
44
  tests/test_evaluator_discovery.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "adversarial-workflow"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "Multi-stage AI code review system preventing phantom work - Author/Evaluator pattern"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -26,7 +26,7 @@ class TestCLISmoke:
26
26
  text=True,
27
27
  )
28
28
  assert result.returncode == 0
29
- assert "0.6.0" in result.stdout or "0.6.0" in result.stderr
29
+ assert "0.6.1" in result.stdout or "0.6.1" in result.stderr
30
30
 
31
31
  def test_help_flag(self):
32
32
  """Test that --help returns help text."""
@@ -0,0 +1,214 @@
1
+ """Tests for .env file loading at CLI startup."""
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+
7
+
8
+ class TestEnvFileLoading:
9
+ """Tests for automatic .env loading."""
10
+
11
+ def test_env_var_available_via_cli_check(self, tmp_path):
12
+ """Verify .env file is loaded when CLI commands run."""
13
+ # Create .env with OPENAI_API_KEY
14
+ (tmp_path / ".env").write_text("OPENAI_API_KEY=sk-test-env-loading\n")
15
+
16
+ # Run check command which validates OPENAI_API_KEY
17
+ # Remove OPENAI_API_KEY from environment to ensure it comes from .env
18
+ env = {k: v for k, v in os.environ.items() if k != "OPENAI_API_KEY"}
19
+ env["PATH"] = os.environ.get("PATH", "")
20
+
21
+ result = subprocess.run(
22
+ [sys.executable, "-m", "adversarial_workflow.cli", "check"],
23
+ capture_output=True,
24
+ text=True,
25
+ cwd=tmp_path,
26
+ env=env,
27
+ )
28
+
29
+ # The check command should see the API key from .env
30
+ # It will show as valid (green checkmark) in the output
31
+ combined_output = result.stdout + result.stderr
32
+ assert "OPENAI_API_KEY" in combined_output, (
33
+ f"Expected OPENAI_API_KEY check. stdout: {result.stdout}, stderr: {result.stderr}"
34
+ )
35
+
36
+ def test_env_loaded_before_evaluator_commands(self, tmp_path, monkeypatch):
37
+ """API keys in .env are available to evaluator commands."""
38
+ # Create .env with test key
39
+ (tmp_path / ".env").write_text("TEST_API_KEY=secret-test-value\n")
40
+
41
+ # Create minimal evaluator config
42
+ eval_dir = tmp_path / ".adversarial" / "evaluators"
43
+ eval_dir.mkdir(parents=True)
44
+ (eval_dir / "test.yml").write_text("""name: test
45
+ description: Test evaluator
46
+ model: gpt-4o-mini
47
+ api_key_env: TEST_API_KEY
48
+ prompt: Test prompt
49
+ output_suffix: TEST
50
+ """)
51
+
52
+ monkeypatch.chdir(tmp_path)
53
+ # Ensure key is NOT in current environment
54
+ monkeypatch.delenv("TEST_API_KEY", raising=False)
55
+
56
+ # list-evaluators should work (loads .env, discovers evaluator)
57
+ result = subprocess.run(
58
+ [sys.executable, "-m", "adversarial_workflow.cli", "list-evaluators"],
59
+ capture_output=True,
60
+ text=True,
61
+ cwd=tmp_path,
62
+ )
63
+
64
+ assert result.returncode == 0
65
+ assert "test" in result.stdout
66
+
67
+ def test_env_loaded_for_builtin_commands(self, tmp_path, monkeypatch):
68
+ """.env is loaded even for built-in commands."""
69
+ # Create .env with OpenAI key
70
+ (tmp_path / ".env").write_text("OPENAI_API_KEY=sk-test-key\n")
71
+
72
+ monkeypatch.chdir(tmp_path)
73
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
74
+
75
+ # check command should find the key from .env
76
+ result = subprocess.run(
77
+ [sys.executable, "-m", "adversarial_workflow.cli", "check"],
78
+ capture_output=True,
79
+ text=True,
80
+ cwd=tmp_path,
81
+ )
82
+
83
+ # Should mention OpenAI (found from .env)
84
+ # The check may fail for other reasons but should see the key
85
+ assert "OPENAI" in result.stdout or "openai" in result.stdout.lower()
86
+
87
+ def test_missing_env_file_no_error(self, tmp_path, monkeypatch):
88
+ """CLI works fine when no .env file exists."""
89
+ monkeypatch.chdir(tmp_path)
90
+
91
+ result = subprocess.run(
92
+ [sys.executable, "-m", "adversarial_workflow.cli", "--help"],
93
+ capture_output=True,
94
+ text=True,
95
+ cwd=tmp_path,
96
+ )
97
+
98
+ assert result.returncode == 0
99
+ assert "adversarial" in result.stdout.lower()
100
+
101
+
102
+ class TestCheckEnvCount:
103
+ """Tests for check() command .env variable counting (ADV-0022).
104
+
105
+ These are CLI integration tests using subprocess to verify end-to-end behavior.
106
+ They test that check() correctly reports .env variable count even after
107
+ main() has already loaded the .env file at startup.
108
+ """
109
+
110
+ def test_check_reports_correct_env_count(self, tmp_path):
111
+ """check() reports correct .env variable count even after main() loads it.
112
+
113
+ This is the primary regression test for ADV-0022. Before the fix,
114
+ check() would report "0 variables" because main() already loaded them.
115
+ """
116
+ # Create .env with 3 variables
117
+ (tmp_path / ".env").write_text(
118
+ "OPENAI_API_KEY=sk-test\n"
119
+ "ANTHROPIC_API_KEY=ant-test\n"
120
+ "CUSTOM_KEY=custom-value\n"
121
+ )
122
+
123
+ # Remove keys from environment to isolate test
124
+ env = {k: v for k, v in os.environ.items()
125
+ if k not in ("OPENAI_API_KEY", "ANTHROPIC_API_KEY", "CUSTOM_KEY")}
126
+ env["PATH"] = os.environ.get("PATH", "")
127
+
128
+ result = subprocess.run(
129
+ [sys.executable, "-m", "adversarial_workflow.cli", "check"],
130
+ capture_output=True,
131
+ text=True,
132
+ cwd=tmp_path,
133
+ env=env,
134
+ )
135
+
136
+ # Should report "3 variables configured", not "0 variables"
137
+ assert "3 variables" in result.stdout, (
138
+ f"Expected '3 variables' in output. Got: {result.stdout}"
139
+ )
140
+
141
+ def test_check_handles_empty_env_file(self, tmp_path):
142
+ """check() handles empty .env file gracefully."""
143
+ (tmp_path / ".env").write_text("")
144
+
145
+ env = {k: v for k, v in os.environ.items()}
146
+ env["PATH"] = os.environ.get("PATH", "")
147
+
148
+ result = subprocess.run(
149
+ [sys.executable, "-m", "adversarial_workflow.cli", "check"],
150
+ capture_output=True,
151
+ text=True,
152
+ cwd=tmp_path,
153
+ env=env,
154
+ )
155
+
156
+ # Should report "0 variables configured"
157
+ assert "0 variables" in result.stdout, (
158
+ f"Expected '0 variables' in output. Got: {result.stdout}"
159
+ )
160
+
161
+ def test_check_handles_comments_in_env(self, tmp_path):
162
+ """check() correctly counts variables, ignoring comments and empty lines."""
163
+ (tmp_path / ".env").write_text(
164
+ "# This is a comment\n"
165
+ "KEY1=value1\n"
166
+ "\n" # Empty line
167
+ "# Another comment\n"
168
+ "KEY2=value2\n"
169
+ )
170
+
171
+ env = {k: v for k, v in os.environ.items() if k not in ("KEY1", "KEY2")}
172
+ env["PATH"] = os.environ.get("PATH", "")
173
+
174
+ result = subprocess.run(
175
+ [sys.executable, "-m", "adversarial_workflow.cli", "check"],
176
+ capture_output=True,
177
+ text=True,
178
+ cwd=tmp_path,
179
+ env=env,
180
+ )
181
+
182
+ # Should report 2 variables (comments and empty lines ignored)
183
+ assert "2 variables" in result.stdout, (
184
+ f"Expected '2 variables' in output. Got: {result.stdout}"
185
+ )
186
+
187
+ def test_check_handles_unusual_env_entries(self, tmp_path):
188
+ """check() handles unusual .env entries without crashing.
189
+
190
+ dotenv_values() treats 'KEY' without = as key with None value.
191
+ This test verifies the CLI doesn't crash on such inputs.
192
+ """
193
+ (tmp_path / ".env").write_text(
194
+ "VALID_KEY=value\n"
195
+ "ALSO_VALID=another\n"
196
+ "KEY_WITHOUT_VALUE\n" # dotenv treats this as KEY_WITHOUT_VALUE=None
197
+ )
198
+
199
+ env = {k: v for k, v in os.environ.items()
200
+ if k not in ("VALID_KEY", "ALSO_VALID", "KEY_WITHOUT_VALUE")}
201
+ env["PATH"] = os.environ.get("PATH", "")
202
+
203
+ result = subprocess.run(
204
+ [sys.executable, "-m", "adversarial_workflow.cli", "check"],
205
+ capture_output=True,
206
+ text=True,
207
+ cwd=tmp_path,
208
+ env=env,
209
+ )
210
+
211
+ # Should not crash - dotenv_values() returns 3 entries (including key with None value)
212
+ assert "3 variables" in result.stdout, (
213
+ f"Expected '3 variables' in output. Got: {result.stdout}"
214
+ )