code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,293 @@
1
+ """
2
+ Basic tests for ChatGPT OAuth plugin.
3
+ """
4
+
5
+ import json
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import pytest
9
+
10
+ from code_puppy.plugins.chatgpt_oauth import config, utils
11
+
12
+
13
+ def test_config_paths():
14
+ """Test configuration path helpers."""
15
+ token_path = config.get_token_storage_path()
16
+ assert token_path.name == "chatgpt_oauth.json"
17
+ # XDG paths use "code_puppy" (without dot) in ~/.local/share or ~/.config
18
+ assert "code_puppy" in str(token_path)
19
+
20
+ config_dir = config.get_config_dir()
21
+ # Default is ~/.code_puppy; XDG paths only used when XDG env vars are set
22
+ assert config_dir.name in ("code_puppy", ".code_puppy")
23
+
24
+ chatgpt_models = config.get_chatgpt_models_path()
25
+ assert chatgpt_models.name == "chatgpt_models.json"
26
+
27
+
28
+ def test_oauth_config():
29
+ """Test OAuth configuration values."""
30
+ assert config.CHATGPT_OAUTH_CONFIG["issuer"] == "https://auth.openai.com"
31
+ assert config.CHATGPT_OAUTH_CONFIG["client_id"] == "app_EMoamEEZ73f0CkXaXp7hrann"
32
+ assert config.CHATGPT_OAUTH_CONFIG["prefix"] == "chatgpt-"
33
+
34
+
35
+ def test_jwt_parsing_with_nested_org():
36
+ """Test JWT parsing with nested organization structure like the user's payload."""
37
+ # This simulates the user's JWT payload structure
38
+ mock_claims = {
39
+ "aud": ["app_EMoamEEZ73f0CkXaXp7hrann"],
40
+ "auth_provider": "google",
41
+ "email": "mike.pfaf fenberger@gmail.com",
42
+ "https://api.openai.com/auth": {
43
+ "chatgpt_account_id": "d1844a91-9aac-419b-903e-f6a99c76f163",
44
+ "organizations": [
45
+ {
46
+ "id": "org-iydWjnSxSr51VuYhDVMDte5",
47
+ "is_default": True,
48
+ "role": "owner",
49
+ "title": "Personal",
50
+ }
51
+ ],
52
+ "groups": ["api-data-sharing-incentives-program", "verified-organization"],
53
+ },
54
+ "sub": "google-oauth2|107692466937587138174",
55
+ }
56
+
57
+ # Test the org extraction logic
58
+ auth_claims = mock_claims.get("https://api.openai.com/auth", {})
59
+ organizations = auth_claims.get("organizations", [])
60
+
61
+ org_id = None
62
+ if organizations:
63
+ default_org = next(
64
+ (org for org in organizations if org.get("is_default")), organizations[0]
65
+ )
66
+ org_id = default_org.get("id")
67
+
68
+ assert org_id == "org-iydWjnSxSr51VuYhDVMDte5"
69
+
70
+ # Test fallback to top-level org_id (should not happen in this case)
71
+ if not org_id:
72
+ org_id = mock_claims.get("organization_id")
73
+
74
+ assert org_id == "org-iydWjnSxSr51VuYhDVMDte5"
75
+ assert config.CHATGPT_OAUTH_CONFIG["required_port"] == 1455
76
+
77
+
78
+ def test_code_verifier_generation():
79
+ """Test PKCE code verifier generation."""
80
+ verifier = utils._generate_code_verifier()
81
+ assert isinstance(verifier, str)
82
+ assert len(verifier) > 50 # Should be long
83
+
84
+
85
+ def test_code_challenge_computation():
86
+ """Test PKCE code challenge computation."""
87
+ verifier = "test_verifier_string"
88
+ challenge = utils._compute_code_challenge(verifier)
89
+ assert isinstance(challenge, str)
90
+ assert len(challenge) > 0
91
+ # Should be URL-safe base64
92
+ assert all(
93
+ c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
94
+ for c in challenge
95
+ )
96
+
97
+
98
+ def test_prepare_oauth_context():
99
+ """Test OAuth context preparation."""
100
+ context = utils.prepare_oauth_context()
101
+ assert context.state
102
+ assert context.code_verifier
103
+ assert context.code_challenge
104
+ assert context.created_at > 0
105
+ assert context.redirect_uri is None
106
+
107
+
108
+ def test_assign_redirect_uri():
109
+ """Test redirect URI assignment."""
110
+ context = utils.prepare_oauth_context()
111
+ redirect_uri = utils.assign_redirect_uri(context, 1455)
112
+ assert redirect_uri == "http://localhost:1455/auth/callback"
113
+ assert context.redirect_uri == redirect_uri
114
+
115
+
116
+ def test_build_authorization_url():
117
+ """Test authorization URL building."""
118
+ context = utils.prepare_oauth_context()
119
+ utils.assign_redirect_uri(context, 1455)
120
+ auth_url = utils.build_authorization_url(context)
121
+
122
+ assert auth_url.startswith("https://auth.openai.com/oauth/authorize?")
123
+ assert "response_type=code" in auth_url
124
+ assert "client_id=" in auth_url
125
+ assert "redirect_uri=" in auth_url
126
+ assert "code_challenge=" in auth_url
127
+ assert "code_challenge_method=S256" in auth_url
128
+ assert f"state={context.state}" in auth_url
129
+
130
+
131
+ def test_parse_jwt_claims():
132
+ """Test JWT claims parsing."""
133
+ # Valid JWT structure (header.payload.signature)
134
+ import base64
135
+
136
+ payload = base64.urlsafe_b64encode(json.dumps({"sub": "user123"}).encode()).decode()
137
+ token = f"header.{payload}.signature"
138
+
139
+ claims = utils.parse_jwt_claims(token)
140
+ assert claims is not None
141
+ assert claims["sub"] == "user123"
142
+
143
+ # Invalid token
144
+ assert utils.parse_jwt_claims("") is None
145
+ assert utils.parse_jwt_claims("invalid") is None
146
+
147
+
148
+ def test_save_and_load_tokens(tmp_path):
149
+ """Test token storage and retrieval."""
150
+ with patch.object(
151
+ config, "get_token_storage_path", return_value=tmp_path / "tokens.json"
152
+ ):
153
+ tokens = {
154
+ "access_token": "test_access",
155
+ "refresh_token": "test_refresh",
156
+ "api_key": "sk-test",
157
+ }
158
+
159
+ # Save tokens
160
+ assert utils.save_tokens(tokens)
161
+
162
+ # Load tokens
163
+ loaded = utils.load_stored_tokens()
164
+ assert loaded == tokens
165
+
166
+
167
+ def test_save_and_load_chatgpt_models(tmp_path):
168
+ """Test ChatGPT models configuration."""
169
+ with patch.object(
170
+ config, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
171
+ ):
172
+ models = {
173
+ "chatgpt-gpt-4o": {
174
+ "type": "openai",
175
+ "name": "gpt-4o",
176
+ "oauth_source": "chatgpt-oauth-plugin",
177
+ }
178
+ }
179
+
180
+ # Save models
181
+ assert utils.save_chatgpt_models(models)
182
+
183
+ # Load models
184
+ loaded = utils.load_chatgpt_models()
185
+ assert loaded == models
186
+
187
+
188
+ def test_remove_chatgpt_models(tmp_path):
189
+ """Test removal of ChatGPT models from config."""
190
+ with patch.object(
191
+ config, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
192
+ ):
193
+ models = {
194
+ "chatgpt-gpt-4o": {
195
+ "type": "openai",
196
+ "oauth_source": "chatgpt-oauth-plugin",
197
+ },
198
+ "claude-3-opus": {
199
+ "type": "anthropic",
200
+ "oauth_source": "other",
201
+ },
202
+ }
203
+ utils.save_chatgpt_models(models)
204
+
205
+ # Remove only ChatGPT models
206
+ removed_count = utils.remove_chatgpt_models()
207
+ assert removed_count == 1
208
+
209
+ # Verify only ChatGPT model was removed
210
+ remaining = utils.load_chatgpt_models()
211
+ assert "chatgpt-gpt-4o" not in remaining
212
+ assert "claude-3-opus" in remaining
213
+
214
+
215
+ @patch("code_puppy.plugins.chatgpt_oauth.utils.requests.post")
216
+ def test_exchange_code_for_tokens(mock_post):
217
+ """Test authorization code exchange."""
218
+ mock_response = MagicMock()
219
+ mock_response.status_code = 200
220
+ mock_response.json.return_value = {
221
+ "access_token": "test_access",
222
+ "refresh_token": "test_refresh",
223
+ "id_token": "test_id",
224
+ }
225
+ mock_post.return_value = mock_response
226
+
227
+ context = utils.prepare_oauth_context()
228
+ utils.assign_redirect_uri(context, 1455)
229
+
230
+ tokens = utils.exchange_code_for_tokens("test_code", context)
231
+ assert tokens is not None
232
+ assert tokens["access_token"] == "test_access"
233
+ assert "last_refresh" in tokens
234
+
235
+
236
+ @patch("code_puppy.plugins.chatgpt_oauth.utils.requests.get")
237
+ def test_fetch_chatgpt_models(mock_get):
238
+ """Test fetching models from ChatGPT Codex API."""
239
+ mock_response = MagicMock()
240
+ mock_response.status_code = 200
241
+ # New response format uses "models" key with "slug" field
242
+ mock_response.json.return_value = {
243
+ "models": [
244
+ {"slug": "gpt-4o"},
245
+ {"slug": "gpt-3.5-turbo"},
246
+ {"slug": "o1-preview"},
247
+ {"slug": "codex-mini"},
248
+ ]
249
+ }
250
+ mock_get.return_value = mock_response
251
+
252
+ models = utils.fetch_chatgpt_models("test_access_token", "test_account_id")
253
+ assert models is not None
254
+ assert "gpt-4o" in models
255
+ assert "gpt-3.5-turbo" in models
256
+ assert "o1-preview" in models
257
+ assert "codex-mini" in models
258
+
259
+
260
+ @patch("code_puppy.plugins.chatgpt_oauth.utils.requests.get")
261
+ def test_fetch_chatgpt_models_fallback(mock_get):
262
+ """Test that fetch_chatgpt_models returns default list on API failure."""
263
+ mock_response = MagicMock()
264
+ mock_response.status_code = 404
265
+ mock_response.text = '{"detail":"Not Found"}'
266
+ mock_get.return_value = mock_response
267
+
268
+ models = utils.fetch_chatgpt_models("test_access_token", "test_account_id")
269
+ assert models is not None
270
+ # Should return default models
271
+ assert "gpt-5.2" in models
272
+ assert "gpt-4o" in models
273
+
274
+
275
+ def test_add_models_to_chatgpt_config(tmp_path):
276
+ """Test adding models to chatgpt_models.json."""
277
+ with patch.object(
278
+ config, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
279
+ ):
280
+ models = ["gpt-4o", "gpt-3.5-turbo"]
281
+
282
+ assert utils.add_models_to_extra_config(models)
283
+
284
+ loaded = utils.load_chatgpt_models()
285
+ assert "chatgpt-gpt-4o" in loaded
286
+ assert "chatgpt-gpt-3.5-turbo" in loaded
287
+ assert loaded["chatgpt-gpt-4o"]["type"] == "chatgpt_oauth"
288
+ assert loaded["chatgpt-gpt-4o"]["name"] == "gpt-4o"
289
+ assert loaded["chatgpt-gpt-4o"]["oauth_source"] == "chatgpt-oauth-plugin"
290
+
291
+
292
+ if __name__ == "__main__":
293
+ pytest.main([__file__, "-v"])