from __future__ import annotations import asyncio from app_server_harness import AppServerHarness from app_server_helpers import response_approval_policy from openai_codex import ApprovalMode, AsyncCodex, Codex from openai_codex.generated.v2_all import AskForApprovalValue, ThreadResumeParams def test_thread_resume_inherits_deny_all_approval_mode(tmp_path) -> None: """Resuming a thread should preserve its stored approval mode.""" with AppServerHarness(tmp_path) as harness: harness.responses.enqueue_assistant_message("source seeded", response_id="resume-mode") with Codex(config=harness.app_server_config()) as codex: source = codex.thread_start(approval_mode=ApprovalMode.deny_all) result = source.run("seed the source rollout") resumed = codex.thread_resume(source.id) resumed_state = codex._client.thread_resume( # noqa: SLF001 resumed.id, ThreadResumeParams(thread_id=resumed.id), ) assert { "final_response": result.final_response, "resumed_policy": response_approval_policy(resumed_state), } == { "final_response": "source seeded", "resumed_policy": AskForApprovalValue.never.value, } def test_thread_fork_inherits_deny_all_approval_mode(tmp_path) -> None: """Forking without an override should preserve the source approval mode.""" with AppServerHarness(tmp_path) as harness: harness.responses.enqueue_assistant_message("source seeded", response_id="fork-mode") with Codex(config=harness.app_server_config()) as codex: source = codex.thread_start(approval_mode=ApprovalMode.deny_all) result = source.run("seed the source rollout") forked = codex.thread_fork(source.id) forked_state = codex._client.thread_resume( # noqa: SLF001 forked.id, ThreadResumeParams(thread_id=forked.id), ) assert { "final_response": result.final_response, "forked_is_distinct": forked.id != source.id, "forked_policy": response_approval_policy(forked_state), } == { "final_response": "source seeded", "forked_is_distinct": True, "forked_policy": AskForApprovalValue.never.value, } def test_thread_fork_can_override_approval_mode(tmp_path) -> None: """Forking with an explicit approval mode should send an override.""" with AppServerHarness(tmp_path) as harness: harness.responses.enqueue_assistant_message( "source seeded", response_id="fork-override-mode", ) with Codex(config=harness.app_server_config()) as codex: source = codex.thread_start(approval_mode=ApprovalMode.deny_all) result = source.run("seed the source rollout") forked = codex.thread_fork( source.id, approval_mode=ApprovalMode.auto_review, ) forked_state = codex._client.thread_resume( # noqa: SLF001 forked.id, ThreadResumeParams(thread_id=forked.id), ) assert { "final_response": result.final_response, "forked_policy": response_approval_policy(forked_state), } == { "final_response": "source seeded", "forked_policy": AskForApprovalValue.on_request.value, } def test_turn_approval_mode_persists_until_next_turn(tmp_path) -> None: """A turn-level approval override should apply to later omitted-arg turns.""" with AppServerHarness(tmp_path) as harness: harness.responses.enqueue_assistant_message("turn override", response_id="turn-mode-1") harness.responses.enqueue_assistant_message("turn inherited", response_id="turn-mode-2") with Codex(config=harness.app_server_config()) as codex: thread = codex.thread_start() first_result = thread.run( "deny this and later turns", approval_mode=ApprovalMode.deny_all, ) after_turn_override = codex._client.thread_resume( # noqa: SLF001 thread.id, ThreadResumeParams(thread_id=thread.id), ) second_result = thread.run("inherit previous approval mode") after_omitted_turn = codex._client.thread_resume( # noqa: SLF001 thread.id, ThreadResumeParams(thread_id=thread.id), ) assert { "after_turn_override": response_approval_policy(after_turn_override), "after_omitted_turn": response_approval_policy(after_omitted_turn), "final_responses": [ first_result.final_response, second_result.final_response, ], } == { "after_turn_override": AskForApprovalValue.never.value, "after_omitted_turn": AskForApprovalValue.never.value, "final_responses": ["turn override", "turn inherited"], } def test_thread_run_approval_mode_persists_until_explicit_override(tmp_path) -> None: """Omitted run approval mode should not rewrite the thread's stored setting.""" with AppServerHarness(tmp_path) as harness: harness.responses.enqueue_assistant_message("locked down", response_id="approval-1") harness.responses.enqueue_assistant_message("reviewable", response_id="approval-2") with Codex(config=harness.app_server_config()) as codex: thread = codex.thread_start(approval_mode=ApprovalMode.deny_all) first_result = thread.run("keep approvals denied") after_default_run = codex._client.thread_resume( # noqa: SLF001 thread.id, ThreadResumeParams(thread_id=thread.id), ) second_result = thread.run( "allow auto review now", approval_mode=ApprovalMode.auto_review, ) after_override_run = codex._client.thread_resume( # noqa: SLF001 thread.id, ThreadResumeParams(thread_id=thread.id), ) assert { "after_default_policy": response_approval_policy(after_default_run), "after_override_policy": response_approval_policy(after_override_run), "final_responses": [ first_result.final_response, second_result.final_response, ], } == { "after_default_policy": AskForApprovalValue.never.value, "after_override_policy": AskForApprovalValue.on_request.value, "final_responses": ["locked down", "reviewable"], } def test_async_thread_run_approval_mode_persists_until_explicit_override( tmp_path, ) -> None: """Async omitted run approval mode should leave stored settings alone.""" async def scenario() -> None: """Use the async client to verify persisted app-server approval state.""" with AppServerHarness(tmp_path) as harness: harness.responses.enqueue_assistant_message( "async locked down", response_id="async-approval-1", ) harness.responses.enqueue_assistant_message( "async reviewable", response_id="async-approval-2", ) async with AsyncCodex(config=harness.app_server_config()) as codex: thread = await codex.thread_start(approval_mode=ApprovalMode.deny_all) first_result = await thread.run("keep async approvals denied") after_default_run = await codex._client.thread_resume( # noqa: SLF001 thread.id, ThreadResumeParams(thread_id=thread.id), ) second_result = await thread.run( "allow async auto review now", approval_mode=ApprovalMode.auto_review, ) after_override_run = await codex._client.thread_resume( # noqa: SLF001 thread.id, ThreadResumeParams(thread_id=thread.id), ) assert { "after_default_policy": response_approval_policy(after_default_run), "after_override_policy": response_approval_policy(after_override_run), "final_responses": [ first_result.final_response, second_result.final_response, ], } == { "after_default_policy": AskForApprovalValue.never.value, "after_override_policy": AskForApprovalValue.on_request.value, "final_responses": ["async locked down", "async reviewable"], } asyncio.run(scenario())