connectonion 0.4.12__py3-none-any.whl → 0.5.1__py3-none-any.whl

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 (74) hide show
  1. connectonion/__init__.py +11 -5
  2. connectonion/agent.py +44 -42
  3. connectonion/cli/commands/init.py +1 -1
  4. connectonion/cli/commands/project_cmd_lib.py +4 -4
  5. connectonion/cli/commands/reset_commands.py +1 -1
  6. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +15 -11
  7. connectonion/cli/templates/minimal/agent.py +2 -2
  8. connectonion/console.py +55 -3
  9. connectonion/events.py +96 -17
  10. connectonion/llm.py +21 -3
  11. connectonion/logger.py +289 -0
  12. connectonion/prompt_files/eval_expected.md +12 -0
  13. connectonion/tool_executor.py +43 -32
  14. connectonion/usage.py +4 -0
  15. connectonion/useful_events_handlers/reflect.py +13 -9
  16. connectonion/useful_plugins/__init__.py +2 -1
  17. connectonion/useful_plugins/calendar_plugin.py +2 -2
  18. connectonion/useful_plugins/eval.py +130 -0
  19. connectonion/useful_plugins/gmail_plugin.py +4 -4
  20. connectonion/useful_plugins/image_result_formatter.py +4 -3
  21. connectonion/useful_plugins/re_act.py +14 -56
  22. connectonion/useful_plugins/shell_approval.py +2 -2
  23. connectonion/useful_tools/memory.py +4 -0
  24. {connectonion-0.4.12.dist-info → connectonion-0.5.1.dist-info}/METADATA +49 -49
  25. {connectonion-0.4.12.dist-info → connectonion-0.5.1.dist-info}/RECORD +27 -71
  26. {connectonion-0.4.12.dist-info → connectonion-0.5.1.dist-info}/WHEEL +1 -2
  27. connectonion/cli/templates/email-agent/.env.example +0 -23
  28. connectonion/cli/templates/email-agent/README.md +0 -240
  29. connectonion/cli/templates/email-agent/agent.py +0 -374
  30. connectonion/cli/templates/email-agent/demo.py +0 -71
  31. connectonion/cli/templates/meta-agent/.env.example +0 -11
  32. connectonion/cli/templates/minimal/.env.example +0 -5
  33. connectonion/cli/templates/playwright/.env.example +0 -5
  34. connectonion-0.4.12.dist-info/top_level.txt +0 -2
  35. tests/__init__.py +0 -0
  36. tests/cli/__init__.py +0 -1
  37. tests/cli/argparse_runner.py +0 -85
  38. tests/cli/conftest.py +0 -5
  39. tests/cli/test_browser_cli.py +0 -61
  40. tests/cli/test_cli.py +0 -143
  41. tests/cli/test_cli_auth_google.py +0 -344
  42. tests/cli/test_cli_auth_microsoft.py +0 -256
  43. tests/cli/test_cli_create.py +0 -283
  44. tests/cli/test_cli_help.py +0 -200
  45. tests/cli/test_cli_init.py +0 -318
  46. tests/conftest.py +0 -283
  47. tests/debug_gemini_models.py +0 -23
  48. tests/fixtures/__init__.py +0 -1
  49. tests/fixtures/test_tools.py +0 -112
  50. tests/fixtures/trust_fixtures.py +0 -257
  51. tests/real_api/__init__.py +0 -0
  52. tests/real_api/conftest.py +0 -9
  53. tests/real_api/test_llm_do.py +0 -174
  54. tests/real_api/test_llm_do_comprehensive.py +0 -527
  55. tests/real_api/test_production_client.py +0 -94
  56. tests/real_api/test_real_anthropic.py +0 -100
  57. tests/real_api/test_real_api.py +0 -113
  58. tests/real_api/test_real_auth.py +0 -130
  59. tests/real_api/test_real_email.py +0 -95
  60. tests/real_api/test_real_gemini.py +0 -96
  61. tests/real_api/test_real_llm_do.py +0 -81
  62. tests/real_api/test_real_managed.py +0 -208
  63. tests/real_api/test_real_multi_llm.py +0 -454
  64. tests/real_api/test_real_openai.py +0 -100
  65. tests/real_api/test_responses_parse.py +0 -88
  66. tests/test_diff_writer.py +0 -126
  67. tests/test_events.py +0 -677
  68. tests/test_gemini_co.py +0 -70
  69. tests/test_image_result_formatter.py +0 -88
  70. tests/test_plugin_system.py +0 -110
  71. tests/utils/__init__.py +0 -1
  72. tests/utils/config_helpers.py +0 -188
  73. tests/utils/mock_helpers.py +0 -237
  74. {connectonion-0.4.12.dist-info → connectonion-0.5.1.dist-info}/entry_points.txt +0 -0
tests/cli/test_cli.py DELETED
@@ -1,143 +0,0 @@
1
- """Simplified tests for CLI init command - focusing on core behavior."""
2
-
3
- import os
4
- import tempfile
5
- import shutil
6
- from pathlib import Path
7
- import pytest
8
-
9
- from .argparse_runner import ArgparseCliRunner
10
-
11
-
12
- class TestCliInit:
13
- """Test the co init command."""
14
-
15
- def setup_method(self):
16
- """Setup test environment."""
17
- self.runner = ArgparseCliRunner()
18
-
19
- def test_init_creates_working_agent(self):
20
- """Test that init creates a working agent setup."""
21
- with self.runner.isolated_filesystem():
22
- # Import here to avoid issues before installation
23
- from connectonion.cli.main import cli
24
-
25
- # Run init with template to create agent.py
26
- result = self.runner.invoke(cli, ['init', '--template', 'minimal'])
27
- assert result.exit_code == 0
28
-
29
- # Check core files exist
30
- assert os.path.exists('agent.py')
31
- assert os.path.exists('.env') # CLI creates .env, not .env.example
32
- assert os.path.exists('.co/config.toml')
33
-
34
- # Verify agent.py is valid Python
35
- with open('agent.py') as f:
36
- code = f.read()
37
- compile(code, 'agent.py', 'exec')
38
-
39
- # Check agent.py references ConnectOnion
40
- assert 'from connectonion import Agent' in code
41
-
42
- # Check config.toml has correct structure
43
- import toml
44
- with open('.co/config.toml') as f:
45
- config = toml.load(f)
46
- assert 'project' in config
47
- assert 'cli' in config
48
-
49
- def test_init_templates(self):
50
- """Test that different templates create appropriate agents."""
51
- with self.runner.isolated_filesystem():
52
- from connectonion.cli.main import cli
53
-
54
- # Test with a template
55
- result = self.runner.invoke(cli, ['init', '--template', 'minimal'])
56
- assert result.exit_code == 0
57
-
58
- with open('agent.py') as f:
59
- content = f.read()
60
-
61
- # Should have basic agent structure
62
- assert 'from connectonion import Agent' in content
63
-
64
- def test_init_in_non_empty_directory(self):
65
- """Test that init asks for confirmation in non-empty directories."""
66
- with self.runner.isolated_filesystem():
67
- from connectonion.cli.main import cli
68
-
69
- # Create existing file
70
- Path('existing.txt').write_text('existing content')
71
-
72
- # Should ask for confirmation and abort when user says no
73
- result = self.runner.invoke(cli, ['init'], input='n\n')
74
- # Check that agent.py was NOT created
75
- if result.exit_code == 0:
76
- # If exit code is 0, user declined, so no agent.py
77
- assert not os.path.exists('agent.py') or result.exit_code != 0
78
-
79
- # Should proceed when user confirms
80
- result = self.runner.invoke(cli, ['init', '--template', 'minimal'], input='y\n')
81
- assert result.exit_code == 0
82
- assert os.path.exists('agent.py')
83
- assert os.path.exists('existing.txt') # Preserves existing files
84
-
85
- def test_init_never_overwrites(self):
86
- """Test that init never overwrites existing agent.py."""
87
- with self.runner.isolated_filesystem():
88
- from connectonion.cli.main import cli
89
-
90
- # Create existing agent.py
91
- Path('agent.py').write_text('# My custom agent')
92
-
93
- # Run init
94
- result = self.runner.invoke(cli, ['init'], input='y\n')
95
-
96
- # Should not overwrite
97
- with open('agent.py') as f:
98
- content = f.read()
99
- assert content == '# My custom agent'
100
-
101
- def test_init_with_git(self):
102
- """Test that init handles git repos properly."""
103
- with self.runner.isolated_filesystem():
104
- from connectonion.cli.main import cli
105
-
106
- # Create .git directory
107
- os.makedirs('.git')
108
-
109
- # Run init (will need confirmation since .git makes it non-empty)
110
- result = self.runner.invoke(cli, ['init'], input='y\n')
111
- assert result.exit_code == 0
112
-
113
- # Should create .gitignore
114
- assert os.path.exists('.gitignore')
115
- with open('.gitignore') as f:
116
- content = f.read()
117
- assert '.env' in content
118
- assert '__pycache__' in content
119
-
120
-
121
- def _co_cli_works():
122
- """Check if 'co --version' actually runs successfully."""
123
- import subprocess
124
- if shutil.which('co') is None:
125
- return False
126
- result = subprocess.run(['co', '--version'], capture_output=True, text=True)
127
- return result.returncode == 0
128
-
129
-
130
- @pytest.mark.skipif(
131
- not _co_cli_works(),
132
- reason="CLI not installed or not working"
133
- )
134
- class TestCliCommands:
135
- """Test actual CLI commands (requires installation)."""
136
-
137
- def test_co_command_works(self):
138
- """Test that 'co' command is available after installation."""
139
- import subprocess
140
- result = subprocess.run(['co', '--version'], capture_output=True, text=True)
141
- assert result.returncode == 0
142
- # Check for version in output (not specific version number)
143
- assert any(c.isdigit() for c in result.stdout)
@@ -1,344 +0,0 @@
1
- """Tests for co auth google CLI command."""
2
-
3
- import os
4
- import tempfile
5
- from pathlib import Path
6
- import pytest
7
- from unittest.mock import Mock, patch, MagicMock
8
-
9
- from .argparse_runner import ArgparseCliRunner
10
-
11
-
12
- class TestAuthGoogleHelp:
13
- """Test help text for co auth google command."""
14
-
15
- def setup_method(self):
16
- """Setup test environment."""
17
- self.runner = ArgparseCliRunner()
18
-
19
- def test_auth_help_shows_google_option(self):
20
- """Test that co auth --help mentions google service."""
21
- from connectonion.cli.main import cli
22
-
23
- result = self.runner.invoke(cli, ['auth', '--help'])
24
- assert result.exit_code == 0
25
- assert 'google' in result.output.lower()
26
-
27
- @patch('connectonion.cli.commands.auth_commands._load_api_key')
28
- def test_auth_google_requires_openonion_auth(self, mock_load_key):
29
- """Test that co auth google requires prior OpenOnion authentication."""
30
- # Mock _load_api_key to return None (no API key found)
31
- mock_load_key.return_value = None
32
-
33
- with self.runner.isolated_filesystem():
34
- from connectonion.cli.main import cli
35
-
36
- # co auth google should fail without OPENONION_API_KEY
37
- result = self.runner.invoke(cli, ['auth', 'google'])
38
- assert 'Not authenticated with OpenOnion' in result.output
39
-
40
-
41
- class TestLoadApiKey:
42
- """Test the _load_api_key helper function."""
43
-
44
- def test_load_api_key_from_env_var(self):
45
- """Test loading API key from environment variable."""
46
- from connectonion.cli.commands.auth_commands import _load_api_key
47
-
48
- with patch.dict(os.environ, {'OPENONION_API_KEY': 'test-key-123'}):
49
- key = _load_api_key()
50
- assert key == 'test-key-123'
51
-
52
- def test_load_api_key_from_local_env(self):
53
- """Test loading API key from local .env file."""
54
- from connectonion.cli.commands.auth_commands import _load_api_key
55
-
56
- with tempfile.TemporaryDirectory() as tmpdir:
57
- os.chdir(tmpdir)
58
-
59
- # Create .env with API key
60
- Path('.env').write_text('OPENONION_API_KEY=local-key-456\n')
61
-
62
- # Clear environment variable
63
- with patch.dict(os.environ, {}, clear=True):
64
- key = _load_api_key()
65
- assert key == 'local-key-456'
66
-
67
- def test_load_api_key_from_global_keys_env(self):
68
- """Test loading API key from global ~/.co/keys.env."""
69
- from connectonion.cli.commands.auth_commands import _load_api_key
70
-
71
- with tempfile.TemporaryDirectory() as tmpdir:
72
- os.chdir(tmpdir)
73
-
74
- # Create mock ~/.co/keys.env
75
- co_dir = Path(tmpdir) / '.co'
76
- co_dir.mkdir()
77
- keys_env = co_dir / 'keys.env'
78
- keys_env.write_text('OPENONION_API_KEY=global-key-789\n')
79
-
80
- # Mock Path.home() to return tmpdir
81
- with patch('pathlib.Path.home', return_value=Path(tmpdir)):
82
- with patch.dict(os.environ, {}, clear=True):
83
- key = _load_api_key()
84
- assert key == 'global-key-789'
85
-
86
- def test_load_api_key_returns_none_when_not_found(self):
87
- """Test that _load_api_key returns None when no key found."""
88
- from connectonion.cli.commands.auth_commands import _load_api_key
89
-
90
- with tempfile.TemporaryDirectory() as tmpdir:
91
- os.chdir(tmpdir)
92
-
93
- # Mock Path.home() to return tmpdir (no keys.env)
94
- with patch('pathlib.Path.home', return_value=Path(tmpdir)):
95
- with patch.dict(os.environ, {}, clear=True):
96
- key = _load_api_key()
97
- assert key is None
98
-
99
-
100
- class TestSaveGoogleToEnv:
101
- """Test the _save_google_to_env helper function."""
102
-
103
- def test_save_google_credentials_to_new_env(self):
104
- """Test saving Google credentials to a new .env file."""
105
- from connectonion.cli.commands.auth_commands import _save_google_to_env
106
-
107
- with tempfile.TemporaryDirectory() as tmpdir:
108
- env_file = Path(tmpdir) / '.env'
109
-
110
- credentials = {
111
- 'access_token': 'ya29.test123',
112
- 'refresh_token': '1//0gtest456',
113
- 'expires_at': '2025-12-31T23:59:59',
114
- 'scopes': 'gmail.send,calendar.readonly',
115
- 'google_email': 'test@gmail.com'
116
- }
117
-
118
- _save_google_to_env(env_file, credentials)
119
-
120
- # Verify file was created
121
- assert env_file.exists()
122
-
123
- # Verify content
124
- content = env_file.read_text()
125
- assert 'GOOGLE_ACCESS_TOKEN=ya29.test123' in content
126
- assert 'GOOGLE_REFRESH_TOKEN=1//0gtest456' in content
127
- assert 'GOOGLE_TOKEN_EXPIRES_AT=2025-12-31T23:59:59' in content
128
- assert 'GOOGLE_SCOPES=gmail.send,calendar.readonly' in content
129
- assert 'GOOGLE_EMAIL=test@gmail.com' in content
130
-
131
- def test_save_google_credentials_updates_existing_env(self):
132
- """Test that saving Google credentials updates existing .env."""
133
- from connectonion.cli.commands.auth_commands import _save_google_to_env
134
-
135
- with tempfile.TemporaryDirectory() as tmpdir:
136
- env_file = Path(tmpdir) / '.env'
137
-
138
- # Create existing .env with old Google credentials
139
- env_file.write_text('''OPENONION_API_KEY=existing-key
140
- GOOGLE_ACCESS_TOKEN=old-token
141
- GOOGLE_REFRESH_TOKEN=old-refresh
142
- GOOGLE_EMAIL=old@gmail.com
143
- OTHER_VAR=keep-this
144
- ''')
145
-
146
- credentials = {
147
- 'access_token': 'new-token',
148
- 'refresh_token': 'new-refresh',
149
- 'expires_at': '2025-12-31T23:59:59',
150
- 'scopes': 'gmail.send',
151
- 'google_email': 'new@gmail.com'
152
- }
153
-
154
- _save_google_to_env(env_file, credentials)
155
-
156
- content = env_file.read_text()
157
-
158
- # Should preserve non-Google variables
159
- assert 'OPENONION_API_KEY=existing-key' in content
160
- assert 'OTHER_VAR=keep-this' in content
161
-
162
- # Should update Google credentials
163
- assert 'GOOGLE_ACCESS_TOKEN=new-token' in content
164
- assert 'GOOGLE_REFRESH_TOKEN=new-refresh' in content
165
- assert 'GOOGLE_EMAIL=new@gmail.com' in content
166
-
167
- # Should not contain old Google credentials
168
- assert 'old-token' not in content
169
- assert 'old-refresh' not in content
170
-
171
- def test_save_google_credentials_file_permissions(self):
172
- """Test that .env file has restrictive permissions on Unix."""
173
- from connectonion.cli.commands.auth_commands import _save_google_to_env
174
- import sys
175
-
176
- if sys.platform == 'win32':
177
- pytest.skip("File permissions test not applicable on Windows")
178
-
179
- with tempfile.TemporaryDirectory() as tmpdir:
180
- env_file = Path(tmpdir) / '.env'
181
-
182
- credentials = {
183
- 'access_token': 'test',
184
- 'refresh_token': 'test',
185
- 'expires_at': '2025-12-31T23:59:59',
186
- 'scopes': 'gmail.send',
187
- 'google_email': 'test@gmail.com'
188
- }
189
-
190
- _save_google_to_env(env_file, credentials)
191
-
192
- # Check file permissions (should be 0o600 = rw-------)
193
- stat = env_file.stat()
194
- assert oct(stat.st_mode)[-3:] == '600'
195
-
196
-
197
- class TestAuthGoogleFlow:
198
- """Test the co auth google flow with mocked backend."""
199
-
200
- def setup_method(self):
201
- """Setup test environment."""
202
- self.runner = ArgparseCliRunner()
203
-
204
- @patch('connectonion.cli.commands.auth_commands.webbrowser')
205
- @patch('connectonion.cli.commands.auth_commands.requests')
206
- def test_auth_google_success_flow(self, mock_requests, mock_webbrowser):
207
- """Test successful Google OAuth flow."""
208
- with self.runner.isolated_filesystem():
209
- # Setup: Create .env with API key
210
- Path('.env').write_text('OPENONION_API_KEY=test-key\n')
211
-
212
- # Mock API responses
213
- mock_revoke_response = Mock()
214
- mock_revoke_response.status_code = 404 # No existing connection to revoke
215
-
216
- mock_init_response = Mock()
217
- mock_init_response.status_code = 200
218
- mock_init_response.json.return_value = {
219
- 'auth_url': 'https://accounts.google.com/o/oauth2/v2/auth?...'
220
- }
221
-
222
- mock_status_response = Mock()
223
- mock_status_response.status_code = 200
224
- mock_status_response.json.return_value = {'connected': True}
225
-
226
- mock_creds_response = Mock()
227
- mock_creds_response.status_code = 200
228
- mock_creds_response.json.return_value = {
229
- 'access_token': 'ya29.test',
230
- 'refresh_token': '1//0g.test',
231
- 'expires_at': '2025-12-31T23:59:59',
232
- 'scopes': 'gmail.send,calendar.readonly',
233
- 'google_email': 'test@gmail.com'
234
- }
235
-
236
- # Setup mock to return different responses
237
- mock_requests.delete.return_value = mock_revoke_response # /google/revoke
238
- mock_requests.get.side_effect = [
239
- mock_init_response, # /google/init
240
- mock_status_response, # /google/status
241
- mock_creds_response # /google/credentials
242
- ]
243
-
244
- # Mock webbrowser.open to not actually open browser
245
- mock_webbrowser.open.return_value = True
246
-
247
- # Mock time.sleep to speed up test
248
- with patch('time.sleep', return_value=None):
249
- from connectonion.cli.main import cli
250
- result = self.runner.invoke(cli, ['auth', 'google'])
251
-
252
- # Verify revoke was called first (to clear any existing connection)
253
- mock_requests.delete.assert_called_once()
254
-
255
- # Verify browser was opened
256
- mock_webbrowser.open.assert_called_once()
257
-
258
- # Verify credentials were saved to .env
259
- env_content = Path('.env').read_text()
260
- assert 'GOOGLE_ACCESS_TOKEN=ya29.test' in env_content
261
- assert 'GOOGLE_REFRESH_TOKEN=1//0g.test' in env_content
262
- assert 'GOOGLE_EMAIL=test@gmail.com' in env_content
263
-
264
- @patch('connectonion.cli.commands.auth_commands.requests')
265
- def test_auth_google_init_failure(self, mock_requests):
266
- """Test handling of OAuth init failure."""
267
- with self.runner.isolated_filesystem():
268
- # Setup: Create .env with API key
269
- Path('.env').write_text('OPENONION_API_KEY=test-key\n')
270
-
271
- # Mock revoke (always called first)
272
- mock_revoke_response = Mock()
273
- mock_revoke_response.status_code = 404
274
- mock_requests.delete.return_value = mock_revoke_response
275
-
276
- # Mock failed init response
277
- mock_response = Mock()
278
- mock_response.status_code = 500
279
- mock_response.text = 'Internal Server Error'
280
- mock_requests.get.return_value = mock_response
281
-
282
- from connectonion.cli.main import cli
283
- result = self.runner.invoke(cli, ['auth', 'google'])
284
-
285
- # Should show error message
286
- assert 'Failed to initialize OAuth' in result.output or result.exit_code != 0
287
-
288
- @patch('connectonion.cli.commands.auth_commands.webbrowser')
289
- @patch('connectonion.cli.commands.auth_commands.requests')
290
- @patch('time.sleep')
291
- def test_auth_google_timeout(self, mock_sleep, mock_requests, mock_webbrowser):
292
- """Test handling of authorization timeout."""
293
- with self.runner.isolated_filesystem():
294
- # Setup: Create .env with API key
295
- Path('.env').write_text('OPENONION_API_KEY=test-key\n')
296
-
297
- # Mock revoke (always called first)
298
- mock_revoke_response = Mock()
299
- mock_revoke_response.status_code = 404
300
- mock_requests.delete.return_value = mock_revoke_response
301
-
302
- # Mock init response
303
- mock_init_response = Mock()
304
- mock_init_response.status_code = 200
305
- mock_init_response.json.return_value = {
306
- 'auth_url': 'https://accounts.google.com/o/oauth2/v2/auth?...'
307
- }
308
-
309
- # Mock status always returns not connected
310
- mock_status_response = Mock()
311
- mock_status_response.status_code = 200
312
- mock_status_response.json.return_value = {'connected': False}
313
-
314
- mock_requests.get.side_effect = [
315
- mock_init_response,
316
- *[mock_status_response] * 60 # Never becomes connected
317
- ]
318
-
319
- from connectonion.cli.main import cli
320
- result = self.runner.invoke(cli, ['auth', 'google'])
321
-
322
- # Should timeout and show error
323
- assert 'timed out' in result.output.lower() or result.exit_code != 0
324
-
325
-
326
- @pytest.mark.cli
327
- @pytest.mark.skip(reason="Integration test requires manual OAuth flow")
328
- class TestAuthGoogleIntegration:
329
- """Integration tests for co auth google (requires installation)."""
330
-
331
- def test_auth_google_command_exists(self):
332
- """Test that 'co auth google' command is recognized."""
333
- import subprocess
334
-
335
- # Just check that the command is recognized (won't work without API key)
336
- result = subprocess.run(
337
- ['co', 'auth', '--help'],
338
- capture_output=True,
339
- text=True,
340
- timeout=5
341
- )
342
-
343
- # Command should show google as an option
344
- assert 'google' in result.stdout.lower() or 'google' in result.stderr.lower()