From 264c4a42d0bdff10b7ea6ae9e4bdd17ca9538660 Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Wed, 21 Oct 2026 19:40:00 +0000 Subject: [PATCH 1/3] Add Codex desktop catalog lifecycle integration tests Cover the shared ~/.codex/config.toml catalog pointer that the Codex desktop app reads: configure twice beside user settings, bare codex app-server model/list, unchanged repeat, revert, and a user-owned catalog pointer that must survive. Co-authored-by: Isaac --- tests/README.md | 2 +- tests/integration/README.md | 7 ++ .../test_ug_configure_codex_desktop.py | 79 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_ug_configure_codex_desktop.py diff --git a/tests/README.md b/tests/README.md index c14c2e96f..0bb2ec063 100644 --- a/tests/README.md +++ b/tests/README.md @@ -707,7 +707,7 @@ desktop GUI, a real `launchctl` or `schtasks` run, or a login/reboot pass. | Follow-up turns and conversation resume | Not covered; reopen proves startup, not conversation resume | | Claude/Codex interactive smart routing | First-prompt routing is covered by the `managed_fixture` banner journeys and CUJ4. The live subagent-only journeys spawn native children and invoke the installed Smart Router skill to verify routing on -> off -> on within one Claude/Codex session. The scheduled harness-latest workflow repeats CUJ4, routing hooks, skill toggles, and banners with both latest agents. Plugin-refresh survival and native daemon/background dispatch remain deferred. | | Full allow/deny tool-permission matrix | Not covered; onboarding/trust uses actual TUI choices | -| Desktop Codex app, Isaac itself, auto-upgrades | Not covered by command forwarding or pinned-version tests | +| Desktop Codex app, Isaac itself, auto-upgrades | Not covered by command forwarding or pinned-version tests. Codex desktop's shared-config catalog pointer lifecycle is covered by `integration/test_ug_configure_codex_desktop.py`; desktop tracing, GUI, and inference are not | | Native macOS/Windows live TUI, managed settings, resize/signals | Windows fresh-install, CLI, local helpers, and the Claude headless gateway journey are advisory; Codex Windows CI is blocked on npm proxy access. Live PTY/TUI, managed settings, and signal behavior still need separate platform implementation and coverage | | Other agents | Live scope is Claude Code and Codex, plus a non-blocking OpenCode headless case | | OpenCode `--model` / `-m` | Unit/component tests cover raw CLI forwarding, configured model selection, launch arguments, rejection of unknown models before starting the native process, and the existing failure when no default model is available; no dedicated live integration journey | diff --git a/tests/integration/README.md b/tests/integration/README.md index 4e188c967..4fcbe6c01 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -26,6 +26,12 @@ selection, and an unrelated profile. It does not launch Desktop. The macOS/Windows GUI, Cowork, model-switching, and browser-authentication work; those GUI and virtualization prerequisites are not satisfied by the headless suite. +Codex desktop has managed-fixture lifecycle coverage in `test_ug_configure_codex_desktop.py`. +It configures twice beside user-owned `~/.codex/config.toml` settings, checks the stable catalog +pointer, an unchanged repeat configure with no restart notice, and `ug revert`; a second case keeps a user-owned catalog pointer through configure and +revert. It does not cover desktop tracing (its systemd/launchd service needs a user session), the +GUI, or inference. + The separate [dedicated-workspace CUJ](../e2e_cuj/AGENTS.md) lives in `tests/e2e_cuj/` and runs directly with pytest. It does not use this suite's runner or config fixtures. It reuses the session, terminal, file-task, and transcript helpers in `utils/`. @@ -458,6 +464,7 @@ test_ug_codex_app_server.py # actual client/server initialize exchan test_ug_smart_routing_hooks.py # live hook contract plus skill-driven subagent toggles test_ug_configure_claude_lifecycle.py # repeat setup, revert, rejected credentials test_ug_configure_claude_desktop.py # native-path Desktop profile lifecycle, no GUI +test_ug_configure_codex_desktop.py # stubbed managed Codex desktop catalog pointer lifecycle, no GUI test_ug_configure_claude_workspace_switch.py # real skills MCP cleanup across two workspaces test_ug_configure_codex_lifecycle.py # repeat setup, revert, rejected credentials test_ug_claude_managed_model_discovery.py # Claude MPS policy cases (claude_mps.json) diff --git a/tests/integration/test_ug_configure_codex_desktop.py b/tests/integration/test_ug_configure_codex_desktop.py new file mode 100644 index 000000000..4646989d2 --- /dev/null +++ b/tests/integration/test_ug_configure_codex_desktop.py @@ -0,0 +1,79 @@ +"""Black-box lifecycle coverage for the Codex desktop app's shared config and model catalog.""" + +import hashlib +import json +import tomllib + +import pytest +from utils.managed import use_managed_config_fixture +from utils.terminal import TerminalProcess + +pytestmark = [pytest.mark.live, pytest.mark.codex, pytest.mark.managed_fixture] + +USER_SETTINGS = "# user-owned Codex settings\n[notice]\nhide_rate_limit_model_nudge = true\n" +RESTART_NOTICE = "codex app-server daemon restart" + + +def _digest(path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def test_ug_configure_codex_desktop_catalog_lifecycle(live_session, workspace): + """Scenario: configure managed Codex twice beside user-owned settings, then revert. + + Expected: the shared `~/.codex/config.toml` that the desktop app reads gains one pointer to + ug's stable catalog and keeps the user's settings; a repeat configure rewrites neither file + and prints no daemon-restart notice; `ug revert` removes the pointer and every ug catalog while + the user's settings survive. The catalog's models and a bare `codex app-server` `model/list` + are covered by `test_ug_configure_managed_codex`. Desktop tracing, GUI rendering, and + inference are not covered. + """ + session = live_session + shared_config = session.home / ".codex" / "config.toml" + catalog_path = session.home / ".ucode" / "codex-model-catalog.json" + # Ordinary pre-existing user settings, not manufactured gateway state. + shared_config.parent.mkdir(parents=True) + shared_config.write_text(USER_SETTINGS) + use_managed_config_fixture(session, "managed_workspace_default") + configure_args = ("configure", "--workspace", workspace, "--skip-upgrade") + + first = session.run(*configure_args, timeout=240) + assert RESTART_NOTICE in " ".join(first.stderr.split()), first.stderr + configured = tomllib.loads(shared_config.read_text()) + assert configured.pop("model_catalog_json") == str(catalog_path), configured + assert configured == tomllib.loads(USER_SETTINGS) + + before = (_digest(shared_config), _digest(catalog_path)) + second = session.run(*configure_args, timeout=240) + assert RESTART_NOTICE not in " ".join(second.stderr.split()), second.stderr + assert (_digest(shared_config), _digest(catalog_path)) == before + + with TerminalProcess(session, "ug", [str(session.binary), "revert"], "desktop-revert") as tty: + tty.finish() + assert tomllib.loads(shared_config.read_text()) == tomllib.loads(USER_SETTINGS) + assert not list(catalog_path.parent.glob("codex-model-catalog*.json")) + + +def test_ug_configure_codex_desktop_keeps_user_catalog(live_session, workspace): + """Scenario: the user already points the desktop app at their own model catalog. + + Expected: configure warns and leaves the user's pointer and catalog file byte-identical, and + revert neither removes the pointer nor deletes the user's file. + """ + session = live_session + shared_config = session.home / ".codex" / "config.toml" + user_catalog = session.home / "my-catalog.json" + # User-owned inputs: a catalog file and the pointer to it. + user_catalog.write_text(json.dumps({"models": []})) + shared_config.parent.mkdir(parents=True) + shared_config.write_text(f'model_catalog_json = "{user_catalog}"\n') + original = (shared_config.read_text(), user_catalog.read_text()) + use_managed_config_fixture(session, "managed_workspace_default") + + result = session.run("configure", "--workspace", workspace, "--skip-upgrade", timeout=240) + assert "custom model catalog" in " ".join(result.stderr.split()), result.stderr + assert (shared_config.read_text(), user_catalog.read_text()) == original + + with TerminalProcess(session, "ug", [str(session.binary), "revert"], "desktop-keep") as tty: + tty.finish() + assert (shared_config.read_text(), user_catalog.read_text()) == original From 16e2bce9cfeeb1fab46a22f0d7097f7dc3130696 Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Wed, 21 Oct 2026 21:34:45 +0000 Subject: [PATCH 2/3] Address review on Codex desktop integration tests - Quote the user catalog path as JSON so the TOML stays valid on Windows. - Drop the live marker, matching the other managed-fixture Codex tests. - Drop the revert half of the repeat-configure test; the managed model discovery test already covers it. - ug revert now deletes ug's stable catalog even when the user's own model_catalog_json pointer is kept, and the user-catalog test checks it. Co-authored-by: Isaac --- src/ucode/agents/codex.py | 7 +++-- tests/README.md | 2 +- tests/integration/README.md | 13 ++++----- .../test_ug_configure_codex_desktop.py | 27 +++++++++---------- tests/test_agent_codex.py | 14 ++++++++++ 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 07714f30e..145b2d83d 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -408,10 +408,9 @@ def revert_legacy_shared_config() -> bool: """ legacy_changed = _strip_legacy_ucode_entries(_legacy_config_path()) app_catalog_changed = detach_app_model_catalog() - if app_catalog_changed and CODEX_MODEL_CATALOG_PATH.exists(): - CODEX_MODEL_CATALOG_PATH.unlink() - # Launch-scoped catalogs from _model_catalog_path; only the reverted ug profile names them. - for path in CODEX_MODEL_CATALOG_PATH.parent.glob(f"{CODEX_MODEL_CATALOG_PATH.stem}-*.json"): + # The stable catalog is written even beside a user's own pointer, and only the reverted ug + # profile names the launch-scoped ones from _model_catalog_path. + for path in CODEX_MODEL_CATALOG_PATH.parent.glob(f"{CODEX_MODEL_CATALOG_PATH.stem}*.json"): path.unlink(missing_ok=True) desktop_tracing_changed = _sync_desktop_tracing(None) return legacy_changed or app_catalog_changed or desktop_tracing_changed diff --git a/tests/README.md b/tests/README.md index 0bb2ec063..13cb5a5b8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -707,7 +707,7 @@ desktop GUI, a real `launchctl` or `schtasks` run, or a login/reboot pass. | Follow-up turns and conversation resume | Not covered; reopen proves startup, not conversation resume | | Claude/Codex interactive smart routing | First-prompt routing is covered by the `managed_fixture` banner journeys and CUJ4. The live subagent-only journeys spawn native children and invoke the installed Smart Router skill to verify routing on -> off -> on within one Claude/Codex session. The scheduled harness-latest workflow repeats CUJ4, routing hooks, skill toggles, and banners with both latest agents. Plugin-refresh survival and native daemon/background dispatch remain deferred. | | Full allow/deny tool-permission matrix | Not covered; onboarding/trust uses actual TUI choices | -| Desktop Codex app, Isaac itself, auto-upgrades | Not covered by command forwarding or pinned-version tests. Codex desktop's shared-config catalog pointer lifecycle is covered by `integration/test_ug_configure_codex_desktop.py`; desktop tracing, GUI, and inference are not | +| Desktop Codex app, Isaac itself, auto-upgrades | Not covered by command forwarding or pinned-version tests. Codex desktop's shared-config catalog pointer (repeat configure and a user-owned pointer) is covered by `integration/test_ug_configure_codex_desktop.py`; desktop tracing, GUI, and inference are not | | Native macOS/Windows live TUI, managed settings, resize/signals | Windows fresh-install, CLI, local helpers, and the Claude headless gateway journey are advisory; Codex Windows CI is blocked on npm proxy access. Live PTY/TUI, managed settings, and signal behavior still need separate platform implementation and coverage | | Other agents | Live scope is Claude Code and Codex, plus a non-blocking OpenCode headless case | | OpenCode `--model` / `-m` | Unit/component tests cover raw CLI forwarding, configured model selection, launch arguments, rejection of unknown models before starting the native process, and the existing failure when no default model is available; no dedicated live integration journey | diff --git a/tests/integration/README.md b/tests/integration/README.md index 4fcbe6c01..20d2e03dd 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -26,11 +26,12 @@ selection, and an unrelated profile. It does not launch Desktop. The macOS/Windows GUI, Cowork, model-switching, and browser-authentication work; those GUI and virtualization prerequisites are not satisfied by the headless suite. -Codex desktop has managed-fixture lifecycle coverage in `test_ug_configure_codex_desktop.py`. -It configures twice beside user-owned `~/.codex/config.toml` settings, checks the stable catalog -pointer, an unchanged repeat configure with no restart notice, and `ug revert`; a second case keeps a user-owned catalog pointer through configure and -revert. It does not cover desktop tracing (its systemd/launchd service needs a user session), the -GUI, or inference. +Codex desktop has managed-fixture coverage in `test_ug_configure_codex_desktop.py`. It +configures twice beside user-owned `~/.codex/config.toml` settings and checks the stable catalog +pointer and an unchanged repeat configure with no restart notice; a second case keeps a user-owned +catalog pointer through configure and revert while revert deletes ug's own catalog. Revert of +ug's pointer is covered by `test_ug_codex_managed_model_discovery.py`. It does not cover desktop +tracing (its systemd/launchd service needs a user session), the GUI, or inference. The separate [dedicated-workspace CUJ](../e2e_cuj/AGENTS.md) lives in `tests/e2e_cuj/` and runs directly with pytest. It does not use this suite's runner or config fixtures. @@ -464,7 +465,7 @@ test_ug_codex_app_server.py # actual client/server initialize exchan test_ug_smart_routing_hooks.py # live hook contract plus skill-driven subagent toggles test_ug_configure_claude_lifecycle.py # repeat setup, revert, rejected credentials test_ug_configure_claude_desktop.py # native-path Desktop profile lifecycle, no GUI -test_ug_configure_codex_desktop.py # stubbed managed Codex desktop catalog pointer lifecycle, no GUI +test_ug_configure_codex_desktop.py # stubbed managed Codex desktop catalog pointer, no GUI test_ug_configure_claude_workspace_switch.py # real skills MCP cleanup across two workspaces test_ug_configure_codex_lifecycle.py # repeat setup, revert, rejected credentials test_ug_claude_managed_model_discovery.py # Claude MPS policy cases (claude_mps.json) diff --git a/tests/integration/test_ug_configure_codex_desktop.py b/tests/integration/test_ug_configure_codex_desktop.py index 4646989d2..599714180 100644 --- a/tests/integration/test_ug_configure_codex_desktop.py +++ b/tests/integration/test_ug_configure_codex_desktop.py @@ -8,7 +8,7 @@ from utils.managed import use_managed_config_fixture from utils.terminal import TerminalProcess -pytestmark = [pytest.mark.live, pytest.mark.codex, pytest.mark.managed_fixture] +pytestmark = [pytest.mark.managed_fixture, pytest.mark.codex] USER_SETTINGS = "# user-owned Codex settings\n[notice]\nhide_rate_limit_model_nudge = true\n" RESTART_NOTICE = "codex app-server daemon restart" @@ -18,15 +18,15 @@ def _digest(path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() -def test_ug_configure_codex_desktop_catalog_lifecycle(live_session, workspace): - """Scenario: configure managed Codex twice beside user-owned settings, then revert. +def test_ug_configure_codex_desktop_repeat_configure(live_session, workspace): + """Scenario: configure managed Codex twice beside user-owned settings. Expected: the shared `~/.codex/config.toml` that the desktop app reads gains one pointer to ug's stable catalog and keeps the user's settings; a repeat configure rewrites neither file - and prints no daemon-restart notice; `ug revert` removes the pointer and every ug catalog while - the user's settings survive. The catalog's models and a bare `codex app-server` `model/list` - are covered by `test_ug_configure_managed_codex`. Desktop tracing, GUI rendering, and - inference are not covered. + and prints no daemon-restart notice. The catalog's models and a bare `codex app-server` + `model/list` are covered by `test_ug_configure_managed_codex`, and revert by + `test_case_02_managed_codex_uses_admin_discovery_after_configure`. Desktop tracing, GUI + rendering, and inference are not covered. """ session = live_session shared_config = session.home / ".codex" / "config.toml" @@ -48,25 +48,21 @@ def test_ug_configure_codex_desktop_catalog_lifecycle(live_session, workspace): assert RESTART_NOTICE not in " ".join(second.stderr.split()), second.stderr assert (_digest(shared_config), _digest(catalog_path)) == before - with TerminalProcess(session, "ug", [str(session.binary), "revert"], "desktop-revert") as tty: - tty.finish() - assert tomllib.loads(shared_config.read_text()) == tomllib.loads(USER_SETTINGS) - assert not list(catalog_path.parent.glob("codex-model-catalog*.json")) - def test_ug_configure_codex_desktop_keeps_user_catalog(live_session, workspace): """Scenario: the user already points the desktop app at their own model catalog. Expected: configure warns and leaves the user's pointer and catalog file byte-identical, and - revert neither removes the pointer nor deletes the user's file. + revert keeps both while deleting the catalog ug wrote beside them. """ session = live_session shared_config = session.home / ".codex" / "config.toml" user_catalog = session.home / "my-catalog.json" - # User-owned inputs: a catalog file and the pointer to it. + # User-owned inputs: a catalog file and the pointer to it. JSON quoting keeps Windows paths + # valid TOML. user_catalog.write_text(json.dumps({"models": []})) shared_config.parent.mkdir(parents=True) - shared_config.write_text(f'model_catalog_json = "{user_catalog}"\n') + shared_config.write_text(f"model_catalog_json = {json.dumps(str(user_catalog))}\n") original = (shared_config.read_text(), user_catalog.read_text()) use_managed_config_fixture(session, "managed_workspace_default") @@ -77,3 +73,4 @@ def test_ug_configure_codex_desktop_keeps_user_catalog(live_session, workspace): with TerminalProcess(session, "ug", [str(session.binary), "revert"], "desktop-keep") as tty: tty.finish() assert (shared_config.read_text(), user_catalog.read_text()) == original + assert not list((session.home / ".ucode").glob("codex-model-catalog*.json")) diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index a76464472..6eda195fe 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -804,6 +804,20 @@ def test_strips_ucode_app_catalog_reference(self, capsys): assert not launch_catalog.exists() assert "codex app-server daemon restart" in " ".join(capsys.readouterr().err.split()) + def test_revert_deletes_ug_catalog_beside_user_pointer(self): + shared_path = codex.CODEX_CONFIG_PATH.parent / "config.toml" + shared_path.parent.mkdir() + user_catalog = shared_path.parent / "mine.json" + user_catalog.write_text("{}", encoding="utf-8") + shared_path.write_text(f"model_catalog_json = {json.dumps(str(user_catalog))}\n") + codex.CODEX_MODEL_CATALOG_PATH.write_text("{}", encoding="utf-8") + + assert codex.revert_legacy_shared_config() is False + + assert read_toml_safe(shared_path) == {"model_catalog_json": str(user_catalog)} + assert user_catalog.exists() + assert not codex.CODEX_MODEL_CATALOG_PATH.exists() + class TestCodexAppCatalog: def test_publish_refresh_and_reattach_preserve_settings_and_report_restart(self, capsys): From cff34b79dd602e89300d6f0447884b3d076be53d Mon Sep 17 00:00:00 2001 From: Andy Xu Date: Wed, 21 Oct 2026 21:58:58 +0000 Subject: [PATCH 3/3] Keep the Codex desktop test PR test-only Drop the ug revert change, its unit test, and the leftover-catalog assertion; this PR only adds integration tests. Co-authored-by: Isaac --- src/ucode/agents/codex.py | 7 ++++--- tests/integration/README.md | 6 +++--- .../integration/test_ug_configure_codex_desktop.py | 3 +-- tests/test_agent_codex.py | 14 -------------- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 145b2d83d..07714f30e 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -408,9 +408,10 @@ def revert_legacy_shared_config() -> bool: """ legacy_changed = _strip_legacy_ucode_entries(_legacy_config_path()) app_catalog_changed = detach_app_model_catalog() - # The stable catalog is written even beside a user's own pointer, and only the reverted ug - # profile names the launch-scoped ones from _model_catalog_path. - for path in CODEX_MODEL_CATALOG_PATH.parent.glob(f"{CODEX_MODEL_CATALOG_PATH.stem}*.json"): + if app_catalog_changed and CODEX_MODEL_CATALOG_PATH.exists(): + CODEX_MODEL_CATALOG_PATH.unlink() + # Launch-scoped catalogs from _model_catalog_path; only the reverted ug profile names them. + for path in CODEX_MODEL_CATALOG_PATH.parent.glob(f"{CODEX_MODEL_CATALOG_PATH.stem}-*.json"): path.unlink(missing_ok=True) desktop_tracing_changed = _sync_desktop_tracing(None) return legacy_changed or app_catalog_changed or desktop_tracing_changed diff --git a/tests/integration/README.md b/tests/integration/README.md index 20d2e03dd..2e7b3af5f 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -29,9 +29,9 @@ virtualization prerequisites are not satisfied by the headless suite. Codex desktop has managed-fixture coverage in `test_ug_configure_codex_desktop.py`. It configures twice beside user-owned `~/.codex/config.toml` settings and checks the stable catalog pointer and an unchanged repeat configure with no restart notice; a second case keeps a user-owned -catalog pointer through configure and revert while revert deletes ug's own catalog. Revert of -ug's pointer is covered by `test_ug_codex_managed_model_discovery.py`. It does not cover desktop -tracing (its systemd/launchd service needs a user session), the GUI, or inference. +catalog pointer through configure and revert. Revert of ug's pointer is covered by +`test_ug_codex_managed_model_discovery.py`. It does not cover desktop tracing (its systemd/launchd +service needs a user session), the GUI, or inference. The separate [dedicated-workspace CUJ](../e2e_cuj/AGENTS.md) lives in `tests/e2e_cuj/` and runs directly with pytest. It does not use this suite's runner or config fixtures. diff --git a/tests/integration/test_ug_configure_codex_desktop.py b/tests/integration/test_ug_configure_codex_desktop.py index 599714180..e628f1ee9 100644 --- a/tests/integration/test_ug_configure_codex_desktop.py +++ b/tests/integration/test_ug_configure_codex_desktop.py @@ -53,7 +53,7 @@ def test_ug_configure_codex_desktop_keeps_user_catalog(live_session, workspace): """Scenario: the user already points the desktop app at their own model catalog. Expected: configure warns and leaves the user's pointer and catalog file byte-identical, and - revert keeps both while deleting the catalog ug wrote beside them. + revert neither removes the pointer nor deletes the user's file. """ session = live_session shared_config = session.home / ".codex" / "config.toml" @@ -73,4 +73,3 @@ def test_ug_configure_codex_desktop_keeps_user_catalog(live_session, workspace): with TerminalProcess(session, "ug", [str(session.binary), "revert"], "desktop-keep") as tty: tty.finish() assert (shared_config.read_text(), user_catalog.read_text()) == original - assert not list((session.home / ".ucode").glob("codex-model-catalog*.json")) diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index 6eda195fe..a76464472 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -804,20 +804,6 @@ def test_strips_ucode_app_catalog_reference(self, capsys): assert not launch_catalog.exists() assert "codex app-server daemon restart" in " ".join(capsys.readouterr().err.split()) - def test_revert_deletes_ug_catalog_beside_user_pointer(self): - shared_path = codex.CODEX_CONFIG_PATH.parent / "config.toml" - shared_path.parent.mkdir() - user_catalog = shared_path.parent / "mine.json" - user_catalog.write_text("{}", encoding="utf-8") - shared_path.write_text(f"model_catalog_json = {json.dumps(str(user_catalog))}\n") - codex.CODEX_MODEL_CATALOG_PATH.write_text("{}", encoding="utf-8") - - assert codex.revert_legacy_shared_config() is False - - assert read_toml_safe(shared_path) == {"model_catalog_json": str(user_catalog)} - assert user_catalog.exists() - assert not codex.CODEX_MODEL_CATALOG_PATH.exists() - class TestCodexAppCatalog: def test_publish_refresh_and_reattach_preserve_settings_and_report_restart(self, capsys):