code-puppy-core-plugins 0.0.2__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 (313) hide show
  1. code_puppy_core_plugins/__init__.py +5 -0
  2. code_puppy_core_plugins/acp/README.md +266 -0
  3. code_puppy_core_plugins/acp/__init__.py +8 -0
  4. code_puppy_core_plugins/acp/agent.py +494 -0
  5. code_puppy_core_plugins/acp/bridge.py +309 -0
  6. code_puppy_core_plugins/acp/capabilities.py +73 -0
  7. code_puppy_core_plugins/acp/commands.py +116 -0
  8. code_puppy_core_plugins/acp/content.py +115 -0
  9. code_puppy_core_plugins/acp/io_delegation.py +231 -0
  10. code_puppy_core_plugins/acp/mcp_config.py +66 -0
  11. code_puppy_core_plugins/acp/permissions.py +159 -0
  12. code_puppy_core_plugins/acp/persistence.py +197 -0
  13. code_puppy_core_plugins/acp/register_callbacks.py +110 -0
  14. code_puppy_core_plugins/acp/replay.py +114 -0
  15. code_puppy_core_plugins/acp/session.py +329 -0
  16. code_puppy_core_plugins/acp/session_config.py +179 -0
  17. code_puppy_core_plugins/acp/state.py +135 -0
  18. code_puppy_core_plugins/agent_skills/__init__.py +22 -0
  19. code_puppy_core_plugins/agent_skills/config.py +208 -0
  20. code_puppy_core_plugins/agent_skills/discovery.py +337 -0
  21. code_puppy_core_plugins/agent_skills/downloader.py +392 -0
  22. code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
  23. code_puppy_core_plugins/agent_skills/installer.py +22 -0
  24. code_puppy_core_plugins/agent_skills/metadata.py +219 -0
  25. code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
  26. code_puppy_core_plugins/agent_skills/provider.py +52 -0
  27. code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
  28. code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
  29. code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
  30. code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
  31. code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
  32. code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
  33. code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
  34. code_puppy_core_plugins/aws_bedrock/config.py +101 -0
  35. code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
  36. code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
  37. code_puppy_core_plugins/azure_foundry/README.md +238 -0
  38. code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
  39. code_puppy_core_plugins/azure_foundry/config.py +126 -0
  40. code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
  41. code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
  42. code_puppy_core_plugins/azure_foundry/token.py +182 -0
  43. code_puppy_core_plugins/azure_foundry/utils.py +375 -0
  44. code_puppy_core_plugins/btw/__init__.py +1 -0
  45. code_puppy_core_plugins/btw/inline_view.py +135 -0
  46. code_puppy_core_plugins/btw/register_callbacks.py +106 -0
  47. code_puppy_core_plugins/btw/side_query.py +91 -0
  48. code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
  49. code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
  50. code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
  51. code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
  52. code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
  53. code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
  54. code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
  55. code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
  56. code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
  57. code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
  58. code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
  59. code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
  60. code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
  61. code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
  62. code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
  63. code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
  64. code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
  65. code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
  66. code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
  67. code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
  68. code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
  69. code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
  70. code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
  71. code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
  72. code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
  73. code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
  74. code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
  75. code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
  76. code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
  77. code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
  78. code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
  79. code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
  80. code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
  81. code_puppy_core_plugins/computer_use/README.md +109 -0
  82. code_puppy_core_plugins/computer_use/__init__.py +1 -0
  83. code_puppy_core_plugins/computer_use/accessibility.py +163 -0
  84. code_puppy_core_plugins/computer_use/activation.py +75 -0
  85. code_puppy_core_plugins/computer_use/backend.py +584 -0
  86. code_puppy_core_plugins/computer_use/backend_types.py +5 -0
  87. code_puppy_core_plugins/computer_use/capture.py +110 -0
  88. code_puppy_core_plugins/computer_use/commands.py +62 -0
  89. code_puppy_core_plugins/computer_use/geometry.py +81 -0
  90. code_puppy_core_plugins/computer_use/inline_image.py +56 -0
  91. code_puppy_core_plugins/computer_use/keycodes.py +47 -0
  92. code_puppy_core_plugins/computer_use/policy.py +144 -0
  93. code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
  94. code_puppy_core_plugins/computer_use/safety.py +18 -0
  95. code_puppy_core_plugins/computer_use/settle.py +64 -0
  96. code_puppy_core_plugins/computer_use/state.py +86 -0
  97. code_puppy_core_plugins/computer_use/tools.py +469 -0
  98. code_puppy_core_plugins/context_indicator/__init__.py +14 -0
  99. code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
  100. code_puppy_core_plugins/context_indicator/usage.py +40 -0
  101. code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
  102. code_puppy_core_plugins/copilot_auth/config.py +93 -0
  103. code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
  104. code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
  105. code_puppy_core_plugins/copilot_auth/utils.py +587 -0
  106. code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
  107. code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
  108. code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
  109. code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
  110. code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
  111. code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
  112. code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
  113. code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
  114. code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
  115. code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
  116. code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
  117. code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
  118. code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
  119. code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
  120. code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
  121. code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
  122. code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
  123. code_puppy_core_plugins/emoji_filter/config.py +26 -0
  124. code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
  125. code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
  126. code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
  127. code_puppy_core_plugins/example_custom_command/README.md +328 -0
  128. code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
  129. code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
  130. code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
  131. code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
  132. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
  133. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
  134. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
  135. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
  136. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
  137. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
  138. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
  139. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
  140. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
  141. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
  142. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
  143. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
  144. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
  145. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
  146. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
  147. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
  148. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
  149. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
  150. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
  151. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
  152. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
  153. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
  154. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
  155. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
  156. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
  157. code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
  158. code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
  159. code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
  160. code_puppy_core_plugins/force_push_guard/detector.py +96 -0
  161. code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
  162. code_puppy_core_plugins/fork/__init__.py +7 -0
  163. code_puppy_core_plugins/fork/register_callbacks.py +445 -0
  164. code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
  165. code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
  166. code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
  167. code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
  168. code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
  169. code_puppy_core_plugins/grok_oauth/config.py +49 -0
  170. code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
  171. code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
  172. code_puppy_core_plugins/grok_oauth/utils.py +195 -0
  173. code_puppy_core_plugins/herdr/README.md +168 -0
  174. code_puppy_core_plugins/herdr/__init__.py +9 -0
  175. code_puppy_core_plugins/herdr/client.py +339 -0
  176. code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
  177. code_puppy_core_plugins/herdr/reporter.py +212 -0
  178. code_puppy_core_plugins/herdr/smoke.py +143 -0
  179. code_puppy_core_plugins/herdr/sources.py +133 -0
  180. code_puppy_core_plugins/hook_creator/__init__.py +1 -0
  181. code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
  182. code_puppy_core_plugins/hook_manager/__init__.py +1 -0
  183. code_puppy_core_plugins/hook_manager/config.py +290 -0
  184. code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
  185. code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
  186. code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
  187. code_puppy_core_plugins/meta_oauth/config.py +55 -0
  188. code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
  189. code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
  190. code_puppy_core_plugins/meta_oauth/utils.py +252 -0
  191. code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
  192. code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
  193. code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
  194. code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
  195. code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
  196. code_puppy_core_plugins/no_tools/__init__.py +0 -0
  197. code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
  198. code_puppy_core_plugins/oauth_pasteback.py +110 -0
  199. code_puppy_core_plugins/oauth_puppy_html.py +194 -0
  200. code_puppy_core_plugins/obsidian_agent/README.md +52 -0
  201. code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
  202. code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
  203. code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
  204. code_puppy_core_plugins/ollama/__init__.py +1 -0
  205. code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
  206. code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
  207. code_puppy_core_plugins/ollama_setup/completer.py +38 -0
  208. code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
  209. code_puppy_core_plugins/plugin_list/__init__.py +0 -0
  210. code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
  211. code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
  212. code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
  213. code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
  214. code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
  215. code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
  216. code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
  217. code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
  218. code_puppy_core_plugins/pop_command/__init__.py +1 -0
  219. code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
  220. code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
  221. code_puppy_core_plugins/prompt_newline/config.py +21 -0
  222. code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
  223. code_puppy_core_plugins/prune/__init__.py +1 -0
  224. code_puppy_core_plugins/prune/prune_menu.py +308 -0
  225. code_puppy_core_plugins/prune/prune_model.py +517 -0
  226. code_puppy_core_plugins/prune/prune_render.py +354 -0
  227. code_puppy_core_plugins/prune/register_callbacks.py +388 -0
  228. code_puppy_core_plugins/puppy_kennel/README.md +203 -0
  229. code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
  230. code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
  231. code_puppy_core_plugins/puppy_kennel/config.py +36 -0
  232. code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
  233. code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
  234. code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
  235. code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
  236. code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
  237. code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
  238. code_puppy_core_plugins/puppy_kennel/state.py +68 -0
  239. code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
  240. code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
  241. code_puppy_core_plugins/puppy_spinner/README.md +93 -0
  242. code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
  243. code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
  244. code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
  245. code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
  246. code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
  247. code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
  248. code_puppy_core_plugins/quick_resume/__init__.py +0 -0
  249. code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
  250. code_puppy_core_plugins/review_pr/__init__.py +1 -0
  251. code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
  252. code_puppy_core_plugins/shell_safety/__init__.py +6 -0
  253. code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
  254. code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
  255. code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
  256. code_puppy_core_plugins/stack_dump/__init__.py +1 -0
  257. code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
  258. code_puppy_core_plugins/statusline/__init__.py +14 -0
  259. code_puppy_core_plugins/statusline/config.py +68 -0
  260. code_puppy_core_plugins/statusline/payload.py +138 -0
  261. code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
  262. code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
  263. code_puppy_core_plugins/statusline/runner.py +122 -0
  264. code_puppy_core_plugins/statusline/statusline_command.py +197 -0
  265. code_puppy_core_plugins/steer_queue/__init__.py +10 -0
  266. code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
  267. code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
  268. code_puppy_core_plugins/subagent_panel/README.md +102 -0
  269. code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
  270. code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
  271. code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
  272. code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
  273. code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
  274. code_puppy_core_plugins/subagent_panel/state.py +231 -0
  275. code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
  276. code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
  277. code_puppy_core_plugins/theme/README.md +86 -0
  278. code_puppy_core_plugins/theme/__init__.py +14 -0
  279. code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
  280. code_puppy_core_plugins/theme/content_styles.py +146 -0
  281. code_puppy_core_plugins/theme/osc_palette.py +178 -0
  282. code_puppy_core_plugins/theme/picker.py +358 -0
  283. code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
  284. code_puppy_core_plugins/theme/register_callbacks.py +348 -0
  285. code_puppy_core_plugins/theme/rich_themes.py +202 -0
  286. code_puppy_core_plugins/theme/themes.py +720 -0
  287. code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
  288. code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
  289. code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
  290. code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
  291. code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
  292. code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
  293. code_puppy_core_plugins/universal_constructor/models.py +138 -0
  294. code_puppy_core_plugins/universal_constructor/provider.py +62 -0
  295. code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
  296. code_puppy_core_plugins/universal_constructor/registry.py +302 -0
  297. code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
  298. code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
  299. code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
  300. code_puppy_core_plugins/wiggum/__init__.py +1 -0
  301. code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
  302. code_puppy_core_plugins/wiggum/judge.py +299 -0
  303. code_puppy_core_plugins/wiggum/judge_config.py +249 -0
  304. code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
  305. code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
  306. code_puppy_core_plugins/wiggum/state.py +89 -0
  307. code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
  308. code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
  309. code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
  310. code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
  311. code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
  312. code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
  313. code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,663 @@
1
+ """
2
+ Basic tests for ChatGPT OAuth plugin.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ from contextlib import nullcontext
8
+ from pathlib import Path
9
+ from unittest.mock import MagicMock, patch
10
+
11
+ import pytest
12
+
13
+ from code_puppy_core_plugins.chatgpt_oauth import (
14
+ config,
15
+ image_generation,
16
+ image_tool,
17
+ utils,
18
+ )
19
+
20
+
21
+ def test_codex_image_generation_posts_codex_payload(tmp_path):
22
+ encoded = "aGVsbG8="
23
+ response = MagicMock()
24
+ response.json.return_value = {"data": [{"b64_json": encoded}]}
25
+ response.raise_for_status.return_value = None
26
+
27
+ with (
28
+ patch.object(image_generation, "get_valid_access_token", return_value="token"),
29
+ patch.object(
30
+ image_generation,
31
+ "load_stored_tokens",
32
+ return_value={"account_id": "account"},
33
+ ),
34
+ patch.object(image_generation.config, "DATA_DIR", str(tmp_path)),
35
+ patch.object(image_generation.requests, "post", return_value=response) as post,
36
+ ):
37
+ output_path = image_generation.generate_image("a red fox")
38
+
39
+ assert output_path.read_bytes() == b"hello"
40
+ assert output_path.parent == tmp_path / "generated_images"
41
+ post.assert_called_once()
42
+ call = post.call_args
43
+ assert call.args[0].endswith("/images/generations")
44
+ assert call.kwargs["json"] == {
45
+ "prompt": "a red fox",
46
+ "background": "auto",
47
+ "model": "gpt-image-2",
48
+ "quality": "auto",
49
+ "size": "auto",
50
+ }
51
+ assert call.kwargs["headers"]["ChatGPT-Account-Id"] == "account"
52
+
53
+
54
+ def test_codex_image_generation_with_references_posts_edits_payload(tmp_path):
55
+ """Reference images must go to /images/edits as [{"image_url": data-url}].
56
+
57
+ The Codex backend rejects every other shape with HTTP 400 (see the
58
+ image_generation module docstring), so this asserts the exact wire format.
59
+ """
60
+ reference = tmp_path / "ref.png"
61
+ reference.write_bytes(b"pngbytes")
62
+ expected_b64 = image_generation.base64.b64encode(b"pngbytes").decode()
63
+
64
+ encoded = "aGVsbG8="
65
+ response = MagicMock()
66
+ response.json.return_value = {"data": [{"b64_json": encoded}]}
67
+ response.raise_for_status.return_value = None
68
+
69
+ with (
70
+ patch.object(image_generation, "get_valid_access_token", return_value="token"),
71
+ patch.object(
72
+ image_generation,
73
+ "load_stored_tokens",
74
+ return_value={"account_id": "account"},
75
+ ),
76
+ patch.object(image_generation.config, "DATA_DIR", str(tmp_path)),
77
+ patch.object(image_generation.requests, "post", return_value=response) as post,
78
+ ):
79
+ image_generation.generate_image("a red fox", [reference])
80
+
81
+ call = post.call_args
82
+ assert call.args[0].endswith("/images/edits")
83
+ payload = call.kwargs["json"]
84
+ assert payload["images"] == [{"image_url": f"data:image/png;base64,{expected_b64}"}]
85
+ assert payload["prompt"] == "a red fox"
86
+ assert payload["model"] == "gpt-image-2"
87
+ # `background` is not accepted by the edits endpoint.
88
+ assert "background" not in payload
89
+
90
+
91
+ def test_codex_image_generation_without_references_still_uses_generations(tmp_path):
92
+ """An empty/omitted reference list must not change existing behavior."""
93
+ response = MagicMock()
94
+ response.json.return_value = {"data": [{"b64_json": "aGVsbG8="}]}
95
+ response.raise_for_status.return_value = None
96
+
97
+ with (
98
+ patch.object(image_generation, "get_valid_access_token", return_value="token"),
99
+ patch.object(
100
+ image_generation,
101
+ "load_stored_tokens",
102
+ return_value={"account_id": "account"},
103
+ ),
104
+ patch.object(image_generation.config, "DATA_DIR", str(tmp_path)),
105
+ patch.object(image_generation.requests, "post", return_value=response) as post,
106
+ ):
107
+ image_generation.generate_image("a red fox", [])
108
+
109
+ assert post.call_args.args[0].endswith("/images/generations")
110
+ assert "images" not in post.call_args.kwargs["json"]
111
+
112
+
113
+ def test_codex_image_generation_rejects_missing_reference(tmp_path):
114
+ with (
115
+ patch.object(image_generation, "get_valid_access_token", return_value="token"),
116
+ patch.object(
117
+ image_generation,
118
+ "load_stored_tokens",
119
+ return_value={"account_id": "account"},
120
+ ),
121
+ patch.object(image_generation.requests, "post") as post,
122
+ pytest.raises(image_generation.CodexImageGenerationError),
123
+ ):
124
+ image_generation.generate_image("a red fox", [tmp_path / "nope.png"])
125
+
126
+ post.assert_not_called()
127
+
128
+
129
+ def test_codex_image_generation_rejects_oversized_reference(tmp_path):
130
+ reference = tmp_path / "huge.png"
131
+ reference.write_bytes(b"x" * 16)
132
+
133
+ with (
134
+ patch.object(image_generation, "get_valid_access_token", return_value="token"),
135
+ patch.object(
136
+ image_generation,
137
+ "load_stored_tokens",
138
+ return_value={"account_id": "account"},
139
+ ),
140
+ patch.object(image_generation, "_MAX_REFERENCE_BYTES", 8),
141
+ patch.object(image_generation.requests, "post") as post,
142
+ pytest.raises(image_generation.CodexImageGenerationError),
143
+ ):
144
+ image_generation.generate_image("a red fox", [reference])
145
+
146
+ post.assert_not_called()
147
+
148
+
149
+ def test_codex_image_generation_requires_auth():
150
+ with (
151
+ patch.object(image_generation, "get_valid_access_token", return_value=None),
152
+ patch.object(image_generation, "load_stored_tokens", return_value={}),
153
+ pytest.raises(image_generation.CodexImageGenerationError, match="codex-auth"),
154
+ ):
155
+ image_generation.generate_image("a red fox")
156
+
157
+
158
+ def test_emit_iterm_image(tmp_path):
159
+ output_path = tmp_path / "puppy.png"
160
+ output_path.write_bytes(b"image-bytes")
161
+ stdout = MagicMock()
162
+ stdout.isatty.return_value = True
163
+
164
+ with (
165
+ patch.dict(
166
+ image_generation.os.environ,
167
+ {"TERM_PROGRAM": "iTerm.app"},
168
+ clear=True,
169
+ ),
170
+ patch.object(image_generation.sys, "stdout", stdout),
171
+ patch(
172
+ "code_puppy.messaging.run_ui.suspended_run_ui",
173
+ return_value=nullcontext(),
174
+ ),
175
+ ):
176
+ assert image_generation.emit_iterm_image(output_path) is True
177
+
178
+ sequence = stdout.write.call_args.args[0]
179
+ assert sequence.startswith("\033]1337;File=name=")
180
+ assert ";inline=1;preserveAspectRatio=1:" in sequence
181
+ assert image_generation.base64.b64encode(b"image-bytes").decode() in sequence
182
+ stdout.flush.assert_called_once()
183
+
184
+
185
+ def test_emit_iterm_image_ignores_other_terminals(tmp_path):
186
+ stdout = MagicMock()
187
+ stdout.isatty.return_value = True
188
+ with (
189
+ patch.dict(image_generation.os.environ, {}, clear=True),
190
+ patch.object(image_generation.sys, "stdout", stdout),
191
+ ):
192
+ assert image_generation.emit_iterm_image(tmp_path / "missing.png") is False
193
+ stdout.write.assert_not_called()
194
+
195
+
196
+ def test_codex_imagegen_command():
197
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
198
+
199
+ with (
200
+ patch.object(
201
+ image_generation,
202
+ "generate_image",
203
+ return_value=Path("/tmp/generated.png"),
204
+ ) as generate,
205
+ patch.object(register_callbacks, "emit_info"),
206
+ patch.object(register_callbacks, "emit_success") as success,
207
+ patch.object(image_generation, "emit_iterm_image") as emit_image,
208
+ ):
209
+ assert (
210
+ register_callbacks._handle_custom_command(
211
+ "/codex-imagegen a red fox", "codex-imagegen"
212
+ )
213
+ is True
214
+ )
215
+
216
+ generate.assert_called_once_with("a red fox")
217
+ success.assert_called_once()
218
+ emit_image.assert_called_once_with(Path("/tmp/generated.png"))
219
+
220
+
221
+ def test_imagegen_skill_and_tool_registration():
222
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
223
+
224
+ with patch.object(
225
+ register_callbacks,
226
+ "load_stored_tokens",
227
+ return_value={"access_token": "token", "account_id": "account"},
228
+ ):
229
+ skills = register_callbacks._register_imagegen_skill()
230
+ assert skills[0]["name"] == "codex-imagegen"
231
+ assert Path(skills[0]["skill_md_path"]).is_file()
232
+ assert register_callbacks._advertise_imagegen_tool("code-puppy") == [
233
+ "codex_imagegen"
234
+ ]
235
+ tools = register_callbacks._register_imagegen_tools()
236
+ assert tools == [
237
+ {"name": "codex_imagegen", "register_func": image_tool.register_codex_imagegen}
238
+ ]
239
+
240
+
241
+ def test_imagegen_skill_is_not_registered_when_logged_out():
242
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
243
+
244
+ with patch.object(register_callbacks, "load_stored_tokens", return_value=None):
245
+ assert register_callbacks._register_imagegen_skill() == []
246
+
247
+
248
+ def test_imagegen_tool_is_not_advertised_when_logged_out():
249
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
250
+
251
+ with patch.object(register_callbacks, "load_stored_tokens", return_value=None):
252
+ assert register_callbacks._advertise_imagegen_tool("code-puppy") == []
253
+
254
+
255
+ def test_logout_reloads_agent_to_unbind_imagegen(tmp_path):
256
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
257
+
258
+ token_path = tmp_path / "tokens.json"
259
+ token_path.write_text("{}")
260
+ with (
261
+ patch.object(
262
+ register_callbacks,
263
+ "load_stored_tokens",
264
+ return_value={"access_token": "token", "account_id": "account"},
265
+ ),
266
+ patch.object(
267
+ register_callbacks, "get_token_storage_path", return_value=token_path
268
+ ),
269
+ patch.object(register_callbacks, "remove_chatgpt_models", return_value=0),
270
+ patch.object(register_callbacks, "emit_info"),
271
+ patch.object(register_callbacks, "emit_success"),
272
+ patch.object(register_callbacks, "_reload_active_agent") as reload_agent,
273
+ patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills,
274
+ ):
275
+ register_callbacks._handle_chatgpt_logout()
276
+
277
+ assert not token_path.exists()
278
+ reload_agent.assert_called_once_with()
279
+ refresh_skills.assert_called_once_with()
280
+
281
+
282
+ def test_logout_does_not_refresh_skills_when_already_logged_out(tmp_path):
283
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
284
+
285
+ token_path = tmp_path / "tokens.json"
286
+ with (
287
+ patch.object(register_callbacks, "load_stored_tokens", return_value=None),
288
+ patch.object(
289
+ register_callbacks, "get_token_storage_path", return_value=token_path
290
+ ),
291
+ patch.object(register_callbacks, "remove_chatgpt_models", return_value=0),
292
+ patch.object(register_callbacks, "emit_info"),
293
+ patch.object(register_callbacks, "emit_success"),
294
+ patch.object(register_callbacks, "_reload_active_agent") as reload_agent,
295
+ patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills,
296
+ ):
297
+ register_callbacks._handle_chatgpt_logout()
298
+
299
+ reload_agent.assert_not_called()
300
+ refresh_skills.assert_not_called()
301
+
302
+
303
+ def test_auth_command_refreshes_skill_cache():
304
+ from code_puppy_core_plugins.chatgpt_oauth import register_callbacks
305
+
306
+ with (
307
+ patch.object(register_callbacks, "run_oauth_flow") as oauth_flow,
308
+ patch.object(register_callbacks, "set_model_and_reload_agent") as set_model,
309
+ patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills,
310
+ ):
311
+ assert (
312
+ register_callbacks._handle_custom_command("/codex-auth", "codex-auth")
313
+ is True
314
+ )
315
+
316
+ oauth_flow.assert_called_once_with()
317
+ set_model.assert_called_once_with("codex-gpt-5.6-sol")
318
+ refresh_skills.assert_called_once_with()
319
+
320
+
321
+ def test_codex_imagegen_agent_tool(tmp_path):
322
+ registered = {}
323
+
324
+ class FakeAgent:
325
+ def tool(self, function):
326
+ registered[function.__name__] = function
327
+ return function
328
+
329
+ image_tool.register_codex_imagegen(FakeAgent())
330
+ output_path = tmp_path / "generated.png"
331
+ with (
332
+ patch.object(
333
+ image_tool, "generate_image", return_value=output_path
334
+ ) as generate,
335
+ patch.object(image_tool, "emit_iterm_image", return_value=True),
336
+ ):
337
+ result = asyncio.run(registered["codex_imagegen"](MagicMock(), "a fox"))
338
+
339
+ assert result == {
340
+ "success": True,
341
+ "path": str(output_path),
342
+ "displayed_inline": True,
343
+ }
344
+ generate.assert_called_once_with("a fox", None)
345
+
346
+
347
+ def test_codex_imagegen_agent_tool_forwards_reference_images(tmp_path):
348
+ registered = {}
349
+
350
+ class FakeAgent:
351
+ def tool(self, function):
352
+ registered[function.__name__] = function
353
+ return function
354
+
355
+ image_tool.register_codex_imagegen(FakeAgent())
356
+ output_path = tmp_path / "generated.png"
357
+ refs = [str(tmp_path / "ref.png")]
358
+ with (
359
+ patch.object(
360
+ image_tool, "generate_image", return_value=output_path
361
+ ) as generate,
362
+ patch.object(image_tool, "emit_iterm_image", return_value=False),
363
+ ):
364
+ result = asyncio.run(
365
+ registered["codex_imagegen"](MagicMock(), "same fox, at night", refs)
366
+ )
367
+
368
+ assert result["success"] is True
369
+ generate.assert_called_once_with("same fox, at night", refs)
370
+
371
+
372
+ def test_config_paths():
373
+ """Test configuration path helpers."""
374
+ token_path = config.get_token_storage_path()
375
+ assert token_path.name == "chatgpt_oauth.json"
376
+ # XDG paths use "code_puppy" (without dot) in ~/.local/share or ~/.config
377
+ assert "code_puppy" in str(token_path)
378
+
379
+ config_dir = config.get_config_dir()
380
+ # Default is ~/.code_puppy; XDG paths only used when XDG env vars are set
381
+ assert config_dir.name in ("code_puppy", ".code_puppy")
382
+
383
+ chatgpt_models = config.get_chatgpt_models_path()
384
+ assert chatgpt_models.name == "chatgpt_models.json"
385
+
386
+
387
+ def test_oauth_config():
388
+ """Test OAuth configuration values."""
389
+ assert config.CHATGPT_OAUTH_CONFIG["issuer"] == "https://auth.openai.com"
390
+ assert config.CHATGPT_OAUTH_CONFIG["client_id"] == "app_EMoamEEZ73f0CkXaXp7hrann"
391
+ assert config.CHATGPT_OAUTH_CONFIG["prefix"] == "codex-"
392
+
393
+
394
+ def test_jwt_parsing_with_nested_org():
395
+ """Test JWT parsing with nested organization structure like the user's payload."""
396
+ # This simulates the user's JWT payload structure
397
+ mock_claims = {
398
+ "aud": ["app_EMoamEEZ73f0CkXaXp7hrann"],
399
+ "auth_provider": "google",
400
+ "email": "mike.pfaf fenberger@gmail.com",
401
+ "https://api.openai.com/auth": {
402
+ "chatgpt_account_id": "d1844a91-9aac-419b-903e-f6a99c76f163",
403
+ "organizations": [
404
+ {
405
+ "id": "org-iydWjnSxSr51VuYhDVMDte5",
406
+ "is_default": True,
407
+ "role": "owner",
408
+ "title": "Personal",
409
+ }
410
+ ],
411
+ "groups": ["api-data-sharing-incentives-program", "verified-organization"],
412
+ },
413
+ "sub": "google-oauth2|107692466937587138174",
414
+ }
415
+
416
+ # Test the org extraction logic
417
+ auth_claims = mock_claims.get("https://api.openai.com/auth", {})
418
+ organizations = auth_claims.get("organizations", [])
419
+
420
+ org_id = None
421
+ if organizations:
422
+ default_org = next(
423
+ (org for org in organizations if org.get("is_default")), organizations[0]
424
+ )
425
+ org_id = default_org.get("id")
426
+
427
+ assert org_id == "org-iydWjnSxSr51VuYhDVMDte5"
428
+
429
+ # Test fallback to top-level org_id (should not happen in this case)
430
+ if not org_id:
431
+ org_id = mock_claims.get("organization_id")
432
+
433
+ assert org_id == "org-iydWjnSxSr51VuYhDVMDte5"
434
+ assert config.CHATGPT_OAUTH_CONFIG["required_port"] == 1455
435
+
436
+
437
+ def test_code_verifier_generation():
438
+ """Test PKCE code verifier generation."""
439
+ verifier = utils._generate_code_verifier()
440
+ assert isinstance(verifier, str)
441
+ assert len(verifier) > 50 # Should be long
442
+
443
+
444
+ def test_code_challenge_computation():
445
+ """Test PKCE code challenge computation."""
446
+ verifier = "test_verifier_string"
447
+ challenge = utils._compute_code_challenge(verifier)
448
+ assert isinstance(challenge, str)
449
+ assert len(challenge) > 0
450
+ # Should be URL-safe base64
451
+ assert all(
452
+ c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
453
+ for c in challenge
454
+ )
455
+
456
+
457
+ def test_prepare_oauth_context():
458
+ """Test OAuth context preparation."""
459
+ context = utils.prepare_oauth_context()
460
+ assert context.state
461
+ assert context.code_verifier
462
+ assert context.code_challenge
463
+ assert context.created_at > 0
464
+ assert context.redirect_uri is None
465
+
466
+
467
+ def test_assign_redirect_uri():
468
+ """Test redirect URI assignment."""
469
+ context = utils.prepare_oauth_context()
470
+ redirect_uri = utils.assign_redirect_uri(context, 1455)
471
+ assert redirect_uri == "http://localhost:1455/auth/callback"
472
+ assert context.redirect_uri == redirect_uri
473
+
474
+
475
+ def test_build_authorization_url():
476
+ """Test authorization URL building."""
477
+ context = utils.prepare_oauth_context()
478
+ utils.assign_redirect_uri(context, 1455)
479
+ auth_url = utils.build_authorization_url(context)
480
+
481
+ assert auth_url.startswith("https://auth.openai.com/oauth/authorize?")
482
+ assert "response_type=code" in auth_url
483
+ assert "client_id=" in auth_url
484
+ assert "redirect_uri=" in auth_url
485
+ assert "code_challenge=" in auth_url
486
+ assert "code_challenge_method=S256" in auth_url
487
+ assert f"state={context.state}" in auth_url
488
+
489
+
490
+ def test_parse_jwt_claims():
491
+ """Test JWT claims parsing."""
492
+ # Valid JWT structure (header.payload.signature)
493
+ import base64
494
+
495
+ payload = base64.urlsafe_b64encode(json.dumps({"sub": "user123"}).encode()).decode()
496
+ token = f"header.{payload}.signature"
497
+
498
+ claims = utils.parse_jwt_claims(token)
499
+ assert claims is not None
500
+ assert claims["sub"] == "user123"
501
+
502
+ # Invalid token
503
+ assert utils.parse_jwt_claims("") is None
504
+ assert utils.parse_jwt_claims("invalid") is None
505
+
506
+
507
+ def test_save_and_load_tokens(tmp_path):
508
+ """Test token storage and retrieval."""
509
+ with patch.object(
510
+ utils, "get_token_storage_path", return_value=tmp_path / "tokens.json"
511
+ ):
512
+ tokens = {
513
+ "access_token": "test_access",
514
+ "refresh_token": "test_refresh",
515
+ "api_key": "sk-test",
516
+ }
517
+
518
+ # Save tokens
519
+ assert utils.save_tokens(tokens)
520
+
521
+ # Load tokens
522
+ loaded = utils.load_stored_tokens()
523
+ assert loaded == tokens
524
+
525
+
526
+ def test_save_and_load_chatgpt_models(tmp_path):
527
+ """Test ChatGPT models configuration."""
528
+ with patch.object(
529
+ utils, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
530
+ ):
531
+ models = {
532
+ "codex-gpt-4o": {
533
+ "type": "openai",
534
+ "name": "gpt-4o",
535
+ "oauth_source": "chatgpt-oauth-plugin",
536
+ }
537
+ }
538
+
539
+ # Save models
540
+ assert utils.save_chatgpt_models(models)
541
+
542
+ # Load models
543
+ loaded = utils.load_chatgpt_models()
544
+ assert loaded == models
545
+
546
+
547
+ def test_remove_chatgpt_models(tmp_path):
548
+ """Test removal of ChatGPT models from config."""
549
+ with patch.object(
550
+ utils, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
551
+ ):
552
+ models = {
553
+ "codex-gpt-4o": {
554
+ "type": "openai",
555
+ "oauth_source": "chatgpt-oauth-plugin",
556
+ },
557
+ "claude-3-opus": {
558
+ "type": "anthropic",
559
+ "oauth_source": "other",
560
+ },
561
+ }
562
+ utils.save_chatgpt_models(models)
563
+
564
+ # Remove only ChatGPT models
565
+ removed_count = utils.remove_chatgpt_models()
566
+ assert removed_count == 1
567
+
568
+ # Verify only ChatGPT model was removed
569
+ remaining = utils.load_chatgpt_models()
570
+ assert "codex-gpt-4o" not in remaining
571
+ assert "claude-3-opus" in remaining
572
+
573
+
574
+ @patch("code_puppy_core_plugins.chatgpt_oauth.utils.requests.post")
575
+ def test_exchange_code_for_tokens(mock_post):
576
+ """Test authorization code exchange."""
577
+ mock_response = MagicMock()
578
+ mock_response.status_code = 200
579
+ mock_response.json.return_value = {
580
+ "access_token": "test_access",
581
+ "refresh_token": "test_refresh",
582
+ "id_token": "test_id",
583
+ }
584
+ mock_post.return_value = mock_response
585
+
586
+ context = utils.prepare_oauth_context()
587
+ utils.assign_redirect_uri(context, 1455)
588
+
589
+ tokens = utils.exchange_code_for_tokens("test_code", context)
590
+ assert tokens is not None
591
+ assert tokens["access_token"] == "test_access"
592
+ assert "last_refresh" in tokens
593
+
594
+
595
+ @patch("code_puppy_core_plugins.chatgpt_oauth.utils.requests.get")
596
+ def test_fetch_chatgpt_models(mock_get):
597
+ """Test fetching models from ChatGPT Codex API."""
598
+ mock_response = MagicMock()
599
+ mock_response.status_code = 200
600
+ # New response format uses "models" key with "slug" field
601
+ mock_response.json.return_value = {
602
+ "models": [
603
+ {"slug": "gpt-4o"},
604
+ {"slug": "gpt-3.5-turbo"},
605
+ {"slug": "o1-preview"},
606
+ {"slug": "codex-mini"},
607
+ ]
608
+ }
609
+ mock_get.return_value = mock_response
610
+
611
+ models = utils.fetch_chatgpt_models("test_access_token", "test_account_id")
612
+ assert models is not None
613
+ # A successful endpoint response is authoritative; fallback models are
614
+ # used only when discovery fails.
615
+ assert "gpt-5.4" not in models
616
+ assert "gpt-5.4-mini" not in models
617
+ # API-returned models are preserved.
618
+ assert "gpt-4o" in models
619
+ assert "gpt-3.5-turbo" in models
620
+ assert "o1-preview" in models
621
+ assert "codex-mini" in models
622
+
623
+
624
+ @patch("code_puppy_core_plugins.chatgpt_oauth.utils.requests.get")
625
+ def test_fetch_chatgpt_models_fallback(mock_get):
626
+ """Test that fetch_chatgpt_models returns default list on API failure."""
627
+ mock_response = MagicMock()
628
+ mock_response.status_code = 404
629
+ mock_response.text = '{"detail":"Not Found"}'
630
+ mock_get.return_value = mock_response
631
+
632
+ models = utils.fetch_chatgpt_models("test_access_token", "test_account_id")
633
+ assert models is not None
634
+ # Keep the fallback aligned with the models currently exposed by Codex.
635
+ assert "gpt-5.4" in models
636
+ assert "gpt-5.4-mini" in models
637
+ assert "gpt-5.3-codex-spark" in models
638
+ assert "codex-auto-review" in models
639
+ assert "gpt-5.3-instant" not in models
640
+ assert "gpt-5.3-codex" not in models
641
+ assert "gpt-5.2-codex" not in models
642
+ assert "gpt-5.2" not in models
643
+
644
+
645
+ def test_add_models_to_chatgpt_config(tmp_path):
646
+ """Test adding models to chatgpt_models.json."""
647
+ with patch.object(
648
+ utils, "get_chatgpt_models_path", return_value=tmp_path / "chatgpt_models.json"
649
+ ):
650
+ models = ["gpt-4o", "gpt-3.5-turbo"]
651
+
652
+ assert utils.add_models_to_extra_config(models)
653
+
654
+ loaded = utils.load_chatgpt_models()
655
+ assert "codex-gpt-4o" in loaded
656
+ assert "codex-gpt-3.5-turbo" in loaded
657
+ assert loaded["codex-gpt-4o"]["type"] == "chatgpt_oauth"
658
+ assert loaded["codex-gpt-4o"]["name"] == "gpt-4o"
659
+ assert loaded["codex-gpt-4o"]["oauth_source"] == "chatgpt-oauth-plugin"
660
+
661
+
662
+ if __name__ == "__main__":
663
+ pytest.main([__file__, "-v"])