oh-my-codex 0.20.0 → 0.20.2
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.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +10 -1
- package/dist/adapt/__tests__/hermes.test.js.map +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
- package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
- package/dist/auth/__tests__/hotswap.test.js +183 -0
- package/dist/auth/__tests__/hotswap.test.js.map +1 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +17 -4
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/paths.js.map +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/lockfile.js.map +1 -1
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/skill-mirror.js.map +1 -1
- package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +56 -4
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +735 -14
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +216 -11
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +49 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +84 -12
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +8 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +129 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +6 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +131 -5
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +2264 -17
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-preferences.test.js +79 -0
- package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +168 -5
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +73 -6
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +37 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +61 -1
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +2067 -26
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +58 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/adapt.js.map +1 -1
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/ask.d.ts +1 -1
- package/dist/cli/ask.d.ts.map +1 -1
- package/dist/cli/ask.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/capabilities.js.map +1 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +636 -161
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +11 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +108 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/mission.js.map +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +18 -0
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
- package/dist/cli/native-hook-claim-journal.js +239 -0
- package/dist/cli/native-hook-claim-journal.js.map +1 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +14 -0
- package/dist/cli/ralplan.d.ts.map +1 -0
- package/dist/cli/ralplan.js +203 -0
- package/dist/cli/ralplan.js.map +1 -0
- package/dist/cli/setup-preferences.d.ts +11 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +32 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +53 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1403 -400
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +2 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +5 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +34 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +8 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +947 -99
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +8 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +1287 -11
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +1049 -135
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +28 -49
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +124 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2142 -265
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +40 -26
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1159 -288
- package/dist/config/generator.js.map +1 -1
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +2 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js.map +1 -1
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/config/team-mode.d.ts.map +1 -1
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
- package/dist/document-refresh/enforcer.js.map +1 -1
- package/dist/exec/followup.js.map +1 -1
- package/dist/goal-workflows/artifacts.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +532 -11
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +5 -5
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/code-simplifier/index.js.map +1 -1
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/events.js.map +1 -1
- package/dist/hooks/extensibility/loader.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
- package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +33 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2228 -119
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts +12 -0
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +41 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-session-provenance.d.ts +87 -0
- package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
- package/dist/hooks/prompt-session-provenance.js +180 -0
- package/dist/hooks/prompt-session-provenance.js.map +1 -0
- package/dist/hooks/session.d.ts +139 -34
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1239 -317
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +146 -2
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +26 -5
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +131 -7
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +98 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +58 -58
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/hermes-server.d.ts +70 -70
- package/dist/mcp/hermes-server.d.ts.map +1 -1
- package/dist/mcp/lifecycle-telemetry.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +34 -34
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +14 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +96 -20
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +47 -47
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.d.ts +4 -4
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +23 -23
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +53 -1
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +5 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/index.d.ts +4 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +3 -9
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/lifecycle-dedupe.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/notifications/tmux-detector.js.map +1 -1
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +8 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/planning/artifact-names.js.map +1 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/client.d.ts.map +1 -1
- package/dist/question/client.js.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/events.js.map +1 -1
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js.map +1 -1
- package/dist/question/ui.js.map +1 -1
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +112 -14
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +3 -3
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +155 -60
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +2 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +1 -2
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/process-tree.js.map +1 -1
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +3 -3
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +5414 -801
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
- package/dist/scripts/build-api.js.map +1 -1
- package/dist/scripts/build-explore-harness.js.map +1 -1
- package/dist/scripts/build-sparkshell.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +825 -172
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
- package/dist/scripts/hook-payload-guard.js.map +1 -1
- package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
- package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
- package/dist/scripts/notify-fallback-delivery.js +419 -0
- package/dist/scripts/notify-fallback-delivery.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +136 -30
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +39 -13
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +122 -12
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts +17 -0
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +73 -11
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
- package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +25 -16
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +15 -13
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts +24 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +266 -90
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +879 -2
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +1528 -7
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/test-reply-listener-live.js.map +1 -1
- package/dist/scripts/test-sparkshell.js.map +1 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/session-history/friction.js.map +1 -1
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/sidecar/index.js.map +1 -1
- package/dist/sidecar/render.js.map +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +64 -15
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +19 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +61 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +49 -55
- package/dist/state/operations.js.map +1 -1
- package/dist/state/paths.d.ts +1 -1
- package/dist/state/paths.d.ts.map +1 -1
- package/dist/state/paths.js +1 -1
- package/dist/state/paths.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +43 -4
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/terminal-normalization.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +20 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +1550 -10
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/adapted-role-binding.d.ts +17 -0
- package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
- package/dist/subagents/adapted-role-binding.js +75 -0
- package/dist/subagents/adapted-role-binding.js.map +1 -0
- package/dist/subagents/role-routing-marker.d.ts +10 -0
- package/dist/subagents/role-routing-marker.d.ts.map +1 -0
- package/dist/subagents/role-routing-marker.js +136 -0
- package/dist/subagents/role-routing-marker.js.map +1 -0
- package/dist/subagents/tracker.d.ts +172 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +1077 -61
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +231 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +401 -1
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +181 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +434 -4
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/api-interop.d.ts +2 -2
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +5 -5
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.js.map +1 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/model-contract.d.ts +20 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +267 -30
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/pane-status.js.map +1 -1
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/reminder-intents.d.ts +1 -1
- package/dist/team/reminder-intents.d.ts.map +1 -1
- package/dist/team/reminder-intents.js.map +1 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +83 -52
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +9 -7
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +69 -28
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch-lock.js.map +1 -1
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/events.js.map +1 -1
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +90 -42
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +4 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +16 -0
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +4 -3
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/repo-deps.js.map +1 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/verifier.js.map +1 -1
- package/dist/visual/constants.d.ts +1 -1
- package/dist/visual/constants.d.ts.map +1 -1
- package/dist/vscode/__tests__/index.test.js.map +1 -1
- package/dist/vscode/index.js.map +1 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/wiki/lint.js.map +1 -1
- package/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
- package/skills/omx-setup/SKILL.md +10 -5
- package/skills/ralph/SKILL.md +4 -2
- package/skills/ralplan/SKILL.md +5 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +20497 -12377
- package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
- package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
- package/src/scripts/codex-native-hook.ts +918 -172
- package/src/scripts/notify-fallback-delivery.ts +416 -0
- package/src/scripts/notify-fallback-watcher.ts +132 -40
- package/src/scripts/notify-hook/auto-nudge.ts +43 -12
- package/src/scripts/notify-hook/managed-tmux.ts +149 -12
- package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
- package/src/scripts/notify-hook/state-io.ts +100 -13
- package/src/scripts/notify-hook/tmux-injection.ts +38 -21
- package/src/scripts/notify-hook/visual-verdict.ts +22 -12
- package/src/scripts/notify-hook.ts +335 -90
- package/src/scripts/smoke-packed-install.ts +1829 -6
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import {
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { buildManagedCodexHooksConfig } from '../../config/codex-hooks.js';
|
|
9
|
+
import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, } from '../../config/codex-hooks.js';
|
|
10
|
+
import { uninstall } from '../uninstall.js';
|
|
11
|
+
import TOML from '@iarna/toml';
|
|
10
12
|
function runOmx(cwd, argv, envOverrides = {}) {
|
|
11
13
|
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
const repoRoot = join(testDir, '..', '..', '..');
|
|
@@ -29,7 +31,45 @@ function runOmx(cwd, argv, envOverrides = {}) {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
function shouldSkipForSpawnPermissions(err) {
|
|
32
|
-
return
|
|
34
|
+
return /(EPERM|EACCES)/i.test(err);
|
|
35
|
+
}
|
|
36
|
+
async function withCwd(cwd, run) {
|
|
37
|
+
const previousCwd = process.cwd();
|
|
38
|
+
process.chdir(cwd);
|
|
39
|
+
try {
|
|
40
|
+
return await run();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
process.chdir(previousCwd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function withDriveQualifiedWindowsCodexHome(wd, run) {
|
|
47
|
+
const previousHome = process.env.HOME;
|
|
48
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
49
|
+
const codexHomeDir = 'C:\\Users\\omx\\.codex';
|
|
50
|
+
process.env.HOME = wd;
|
|
51
|
+
process.env.CODEX_HOME = codexHomeDir;
|
|
52
|
+
try {
|
|
53
|
+
return await withCwd(wd, async () => {
|
|
54
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
55
|
+
return run(codexHomeDir);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (typeof previousHome === 'string')
|
|
60
|
+
process.env.HOME = previousHome;
|
|
61
|
+
else
|
|
62
|
+
delete process.env.HOME;
|
|
63
|
+
if (typeof previousCodexHome === 'string') {
|
|
64
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
delete process.env.CODEX_HOME;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function packageRoot() {
|
|
72
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
33
73
|
}
|
|
34
74
|
/** Build a realistic OMX config.toml for testing */
|
|
35
75
|
function buildOmxConfig() {
|
|
@@ -186,6 +226,13 @@ function buildMixedConfig() {
|
|
|
186
226
|
'command = "custom"',
|
|
187
227
|
'args = ["--flag"]',
|
|
188
228
|
'',
|
|
229
|
+
'[agents]',
|
|
230
|
+
'max_threads = 17',
|
|
231
|
+
'max_depth = 5',
|
|
232
|
+
'',
|
|
233
|
+
'[agents.custom_role]',
|
|
234
|
+
'description = "keep me"',
|
|
235
|
+
'',
|
|
189
236
|
'# ============================================================',
|
|
190
237
|
'# oh-my-codex (OMX) Configuration',
|
|
191
238
|
'# Managed by omx setup - manual edits preserved on next setup',
|
|
@@ -228,6 +275,38 @@ function buildMixedConfig() {
|
|
|
228
275
|
'',
|
|
229
276
|
].join('\n');
|
|
230
277
|
}
|
|
278
|
+
function buildAmbiguousMultiAgentConfig(marker, variant) {
|
|
279
|
+
return [
|
|
280
|
+
`sentinel = "${marker}"`,
|
|
281
|
+
'',
|
|
282
|
+
'[features]',
|
|
283
|
+
`multi_agent = ${variant.multiAgent}`,
|
|
284
|
+
'child_agents_md = true',
|
|
285
|
+
'goals = true',
|
|
286
|
+
'custom_feature = true',
|
|
287
|
+
'',
|
|
288
|
+
'[agents]',
|
|
289
|
+
`max_threads = ${variant.maxThreads}`,
|
|
290
|
+
`max_depth = ${variant.maxDepth}`,
|
|
291
|
+
'',
|
|
292
|
+
'[agents.custom_role]',
|
|
293
|
+
`description = "${marker}"`,
|
|
294
|
+
'',
|
|
295
|
+
'# ============================================================',
|
|
296
|
+
'# oh-my-codex (OMX) Configuration',
|
|
297
|
+
'# Managed by omx setup - manual edits preserved on next setup',
|
|
298
|
+
'# ============================================================',
|
|
299
|
+
'',
|
|
300
|
+
'[mcp_servers.omx_state]',
|
|
301
|
+
'command = "node"',
|
|
302
|
+
'args = ["/path/to/state-server.js"]',
|
|
303
|
+
'enabled = true',
|
|
304
|
+
'',
|
|
305
|
+
'# ============================================================',
|
|
306
|
+
'# End oh-my-codex',
|
|
307
|
+
'',
|
|
308
|
+
].join('\n');
|
|
309
|
+
}
|
|
231
310
|
describe('omx uninstall', () => {
|
|
232
311
|
it('removes OMX block from config.toml with --dry-run', async () => {
|
|
233
312
|
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-'));
|
|
@@ -279,7 +358,7 @@ describe('omx uninstall', () => {
|
|
|
279
358
|
assert.doesNotMatch(config, /notify\s*=/);
|
|
280
359
|
assert.doesNotMatch(config, /model_reasoning_effort\s*=/);
|
|
281
360
|
assert.doesNotMatch(config, /developer_instructions\s*=/);
|
|
282
|
-
assert.
|
|
361
|
+
assert.match(config, /multi_agent\s*=/);
|
|
283
362
|
assert.doesNotMatch(config, /child_agents_md\s*=/);
|
|
284
363
|
assert.doesNotMatch(config, /^hooks\s*=/m);
|
|
285
364
|
assert.equal(existsSync(join(codexDir, 'hooks.json')), false);
|
|
@@ -318,6 +397,8 @@ describe('omx uninstall', () => {
|
|
|
318
397
|
managedBy: 'oh-my-codex',
|
|
319
398
|
version: 1,
|
|
320
399
|
previousNotify: ['node', staleDispatcher, '--metadata', metadataPath],
|
|
400
|
+
omxNotify: ['node', join(stalePkgRoot, 'dist', 'scripts', 'notify-hook.js')],
|
|
401
|
+
dispatcherNotify: ['node', staleDispatcher, '--metadata', metadataPath],
|
|
321
402
|
}));
|
|
322
403
|
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
323
404
|
if (shouldSkipForSpawnPermissions(res.error))
|
|
@@ -349,11 +430,17 @@ describe('omx uninstall', () => {
|
|
|
349
430
|
assert.match(config, /model = "o4-mini"/);
|
|
350
431
|
assert.match(config, /\[mcp_servers\.user_custom\]/);
|
|
351
432
|
assert.match(config, /web_search = true/);
|
|
433
|
+
assert.match(config, /^multi_agent = true$/m);
|
|
434
|
+
assert.match(config, /^\[agents\]$/m);
|
|
435
|
+
assert.match(config, /^max_threads = 17$/m);
|
|
436
|
+
assert.match(config, /^max_depth = 5$/m);
|
|
437
|
+
assert.match(config, /^\[agents\.custom_role\]$/m);
|
|
438
|
+
assert.match(config, /^description = "keep me"$/m);
|
|
352
439
|
// OMX entries removed
|
|
353
440
|
assert.doesNotMatch(config, /omx_state/);
|
|
354
441
|
assert.doesNotMatch(config, /omx_memory/);
|
|
355
442
|
assert.doesNotMatch(config, /notify\s*=.*node/);
|
|
356
|
-
assert.
|
|
443
|
+
assert.match(config, /multi_agent\s*=/);
|
|
357
444
|
assert.doesNotMatch(config, /child_agents_md/);
|
|
358
445
|
assert.doesNotMatch(config, /^hooks\s*=/m);
|
|
359
446
|
}
|
|
@@ -361,41 +448,1493 @@ describe('omx uninstall', () => {
|
|
|
361
448
|
await rm(wd, { recursive: true, force: true });
|
|
362
449
|
}
|
|
363
450
|
});
|
|
364
|
-
it('
|
|
365
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-'));
|
|
451
|
+
it('fails closed for unsupported root metadata in hooks.json', async () => {
|
|
452
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-invalid-hooks-root-'));
|
|
366
453
|
try {
|
|
367
454
|
const home = join(wd, 'home');
|
|
368
455
|
const codexDir = join(home, '.codex');
|
|
456
|
+
const configPath = join(codexDir, 'config.toml');
|
|
457
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
458
|
+
const config = buildOmxConfig().replace(/^hooks = true$/m, 'codex_hooks = true');
|
|
459
|
+
const hooks = JSON.stringify({
|
|
460
|
+
version: 1,
|
|
461
|
+
hooks: {
|
|
462
|
+
SessionStart: [{
|
|
463
|
+
hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }],
|
|
464
|
+
}],
|
|
465
|
+
},
|
|
466
|
+
}, null, 2) + '\n';
|
|
369
467
|
await mkdir(codexDir, { recursive: true });
|
|
370
|
-
await writeFile(
|
|
371
|
-
await writeFile(
|
|
468
|
+
await writeFile(configPath, config);
|
|
469
|
+
await writeFile(hooksPath, hooks);
|
|
470
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
471
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
472
|
+
return;
|
|
473
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
474
|
+
assert.match(res.stderr, /unknown root field version/);
|
|
475
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
476
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
await rm(wd, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
it('removes safely positioned managed wrappers while preserving foreign hooks and the native feature flag', async () => {
|
|
483
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-safe-foreign-hooks-'));
|
|
484
|
+
try {
|
|
485
|
+
const home = join(wd, 'home');
|
|
486
|
+
const codexDir = join(home, '.codex');
|
|
487
|
+
const configPath = join(codexDir, 'config.toml');
|
|
488
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
489
|
+
await mkdir(codexDir, { recursive: true });
|
|
490
|
+
await writeFile(configPath, buildOmxConfig().replace(/^hooks = true$/m, 'codex_hooks = true'));
|
|
491
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
372
492
|
hooks: {
|
|
373
493
|
SessionStart: [
|
|
374
|
-
{
|
|
494
|
+
{ hooks: [{ type: 'command', command: 'echo keep-me' }] },
|
|
495
|
+
{ matcher: 'startup|resume|clear', hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }] },
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
}, null, 2) + '\n');
|
|
499
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
500
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
501
|
+
return;
|
|
502
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
503
|
+
const hooks = await readFile(hooksPath, 'utf-8');
|
|
504
|
+
assert.match(hooks, /echo keep-me/);
|
|
505
|
+
assert.doesNotMatch(hooks, /codex-native-hook\.js/);
|
|
506
|
+
const config = await readFile(configPath, 'utf-8');
|
|
507
|
+
assert.match(config, /^hooks = true$/m);
|
|
508
|
+
assert.doesNotMatch(config, /^codex_hooks\s*=/m);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
await rm(wd, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
it('fails closed without removing a shell-expanding foreign command that resembles an OMX hook', async () => {
|
|
515
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-shell-expanding-hook-'));
|
|
516
|
+
try {
|
|
517
|
+
const home = join(wd, 'home');
|
|
518
|
+
const codexDir = join(home, '.codex');
|
|
519
|
+
const configPath = join(codexDir, 'config.toml');
|
|
520
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
521
|
+
const config = buildOmxConfig();
|
|
522
|
+
const hooks = JSON.stringify({
|
|
523
|
+
hooks: {
|
|
524
|
+
SessionStart: [{
|
|
525
|
+
matcher: 'startup|resume|clear',
|
|
526
|
+
hooks: [{ type: 'command', command: 'node "$HOME/repo/dist/scripts/codex-native-hook.js"' }],
|
|
527
|
+
}],
|
|
528
|
+
},
|
|
529
|
+
}, null, 2) + '\n';
|
|
530
|
+
await mkdir(codexDir, { recursive: true });
|
|
531
|
+
await writeFile(configPath, config);
|
|
532
|
+
await writeFile(hooksPath, hooks);
|
|
533
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
534
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
535
|
+
return;
|
|
536
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
537
|
+
assert.match(res.stderr, /ambiguous_managed_handler|does not match the managed command grammar/i);
|
|
538
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
539
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
await rm(wd, { recursive: true, force: true });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
it('fails closed without cleaning config when managed removal would shift a foreign handler', async () => {
|
|
546
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-unsafe-foreign-hooks-'));
|
|
547
|
+
try {
|
|
548
|
+
const home = join(wd, 'home');
|
|
549
|
+
const codexDir = join(home, '.codex');
|
|
550
|
+
const configPath = join(codexDir, 'config.toml');
|
|
551
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
552
|
+
const config = buildOmxConfig();
|
|
553
|
+
const hooks = JSON.stringify({
|
|
554
|
+
hooks: {
|
|
555
|
+
SessionStart: [{
|
|
556
|
+
matcher: 'startup|resume|clear',
|
|
375
557
|
hooks: [
|
|
376
558
|
{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' },
|
|
377
559
|
{ type: 'command', command: 'echo keep-me' },
|
|
378
560
|
],
|
|
561
|
+
}],
|
|
562
|
+
},
|
|
563
|
+
}, null, 2) + '\n';
|
|
564
|
+
await mkdir(codexDir, { recursive: true });
|
|
565
|
+
await writeFile(configPath, config);
|
|
566
|
+
await writeFile(hooksPath, hooks);
|
|
567
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
568
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
569
|
+
return;
|
|
570
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
571
|
+
assert.match(res.stderr, /unsafe_managed_removal|shift a foreign coordinate/i);
|
|
572
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
573
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
await rm(wd, { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
it('fails before uninstall writes when marker-wrapped trust state has foreign siblings', async () => {
|
|
580
|
+
const representations = [
|
|
581
|
+
{
|
|
582
|
+
name: 'inline',
|
|
583
|
+
render: (key, hash) => [
|
|
584
|
+
`hooks = { state = { ${JSON.stringify(key)} = { trusted_hash = ${JSON.stringify(hash)}, foreign = true } } }`,
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'dotted',
|
|
589
|
+
render: (key, hash) => [
|
|
590
|
+
`hooks.state.${JSON.stringify(key)}.trusted_hash = ${JSON.stringify(hash)}`,
|
|
591
|
+
`hooks.state.${JSON.stringify(key)}.foreign = true`,
|
|
592
|
+
],
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: 'table',
|
|
596
|
+
render: (key, hash) => [
|
|
597
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
598
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
599
|
+
'foreign = true',
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'separate-foreign-table',
|
|
604
|
+
render: (key, hash) => [
|
|
605
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
606
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
607
|
+
'',
|
|
608
|
+
'[hooks.state."foreign-key"]',
|
|
609
|
+
'trusted_hash = "sha256:foreign"',
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
];
|
|
613
|
+
for (const representation of representations) {
|
|
614
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-trust-${representation.name}-`));
|
|
615
|
+
try {
|
|
616
|
+
await withCwd(wd, async () => {
|
|
617
|
+
const codexDir = join(wd, '.codex');
|
|
618
|
+
const configPath = join(codexDir, 'config.toml');
|
|
619
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
620
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
621
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
622
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
623
|
+
platform: 'win32',
|
|
624
|
+
codexHomeDir: codexDir,
|
|
625
|
+
}), null, 2)}\n`;
|
|
626
|
+
const [key, trust] = Object.entries(buildManagedCodexHookTrustState(hooksPath, packageRoot(), {
|
|
627
|
+
platform: 'win32',
|
|
628
|
+
codexHomeDir: codexDir,
|
|
629
|
+
hooksContent: hooks,
|
|
630
|
+
}))[0] ?? [];
|
|
631
|
+
assert.ok(key);
|
|
632
|
+
assert.ok(trust);
|
|
633
|
+
const config = [
|
|
634
|
+
'model = "foreign"',
|
|
635
|
+
'',
|
|
636
|
+
'# ============================================================',
|
|
637
|
+
'# oh-my-codex (OMX) Configuration',
|
|
638
|
+
'# Managed by omx setup - manual edits preserved on next setup',
|
|
639
|
+
'# ============================================================',
|
|
640
|
+
'',
|
|
641
|
+
...representation.render(key, trust.trusted_hash),
|
|
642
|
+
'',
|
|
643
|
+
'# ============================================================',
|
|
644
|
+
'# End oh-my-codex',
|
|
645
|
+
'',
|
|
646
|
+
].join('\n');
|
|
647
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', 'utf-8');
|
|
648
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
649
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
650
|
+
await writeFile(configPath, config);
|
|
651
|
+
await writeFile(hooksPath, hooks);
|
|
652
|
+
await writeFile(shimPath, shim);
|
|
653
|
+
await writeFile(metadataPath, metadata);
|
|
654
|
+
await assert.rejects(uninstall({ scope: 'project', transactionPlatform: 'win32' }), (error) => typeof error === 'object' &&
|
|
655
|
+
error !== null &&
|
|
656
|
+
'code' in error &&
|
|
657
|
+
error.code === 'managed_trust_key_conflict');
|
|
658
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, 'utf-8'));
|
|
659
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, 'utf-8'));
|
|
660
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
661
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
662
|
+
assert.deepEqual((await readdir(codexDir)).filter((entry) => entry.includes('.omx-')), []);
|
|
663
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes('.omx-')), []);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
await rm(wd, { recursive: true, force: true });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
it('fails before uninstall writes when marker trust has absent or non-managed hooks expectations', async () => {
|
|
672
|
+
const fixtures = [
|
|
673
|
+
{ name: 'absent', hooks: null },
|
|
674
|
+
{
|
|
675
|
+
name: 'non-managed',
|
|
676
|
+
hooks: `${JSON.stringify({
|
|
677
|
+
hooks: {
|
|
678
|
+
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'echo foreign' }] }],
|
|
679
|
+
},
|
|
680
|
+
}, null, 2)}\n`,
|
|
681
|
+
},
|
|
682
|
+
];
|
|
683
|
+
for (const fixture of fixtures) {
|
|
684
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-marker-${fixture.name}-`));
|
|
685
|
+
try {
|
|
686
|
+
await withCwd(wd, async () => {
|
|
687
|
+
const codexDir = join(wd, '.codex');
|
|
688
|
+
const configPath = join(codexDir, 'config.toml');
|
|
689
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
690
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
691
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
692
|
+
const config = buildOmxConfig().replace('# End oh-my-codex', '[hooks.state."foreign-key"]\ntrusted_hash = "sha256:foreign"\n\n# End oh-my-codex');
|
|
693
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
694
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', 'utf-8');
|
|
695
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
696
|
+
await writeFile(configPath, config);
|
|
697
|
+
if (fixture.hooks !== null)
|
|
698
|
+
await writeFile(hooksPath, fixture.hooks);
|
|
699
|
+
await writeFile(shimPath, shim);
|
|
700
|
+
await writeFile(metadataPath, metadata);
|
|
701
|
+
await assert.rejects(uninstall({ scope: 'project', transactionPlatform: 'win32' }), (error) => typeof error === 'object' &&
|
|
702
|
+
error !== null &&
|
|
703
|
+
'code' in error &&
|
|
704
|
+
error.code === 'managed_trust_key_conflict');
|
|
705
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, 'utf-8'));
|
|
706
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
707
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
708
|
+
if (fixture.hooks === null) {
|
|
709
|
+
assert.equal(existsSync(hooksPath), false);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(fixture.hooks, 'utf-8'));
|
|
713
|
+
}
|
|
714
|
+
assert.deepEqual((await readdir(codexDir)).filter((entry) => entry.includes('.omx-')), []);
|
|
715
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes('.omx-')), []);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
await rm(wd, { recursive: true, force: true });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
it('preserves a shim referenced by a Unicode-escaped future hook event', async () => {
|
|
724
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-future-shim-'));
|
|
725
|
+
try {
|
|
726
|
+
await withCwd(wd, async () => {
|
|
727
|
+
const codexDir = join(wd, '.codex');
|
|
728
|
+
const configPath = join(codexDir, 'config.toml');
|
|
729
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
730
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
731
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
732
|
+
platform: 'win32',
|
|
733
|
+
codexHomeDir: codexDir,
|
|
734
|
+
});
|
|
735
|
+
const sentinel = '__OMX_ESCAPED_SHIM_COMMAND__';
|
|
736
|
+
const hooks = JSON.stringify({
|
|
737
|
+
...managed,
|
|
738
|
+
hooks: {
|
|
739
|
+
...managed.hooks,
|
|
740
|
+
FutureEvent: [{ hooks: [{ type: 'command', command: sentinel }] }],
|
|
741
|
+
},
|
|
742
|
+
}, null, 2).replace(JSON.stringify(sentinel), JSON.stringify(`& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`).replace(/\\\\/g, '\\u005c')) + '\n';
|
|
743
|
+
await mkdir(codexDir, { recursive: true });
|
|
744
|
+
await writeFile(configPath, buildOmxConfig());
|
|
745
|
+
await writeFile(hooksPath, hooks);
|
|
746
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
747
|
+
await uninstall({ scope: 'project', transactionPlatform: 'win32' });
|
|
748
|
+
assert.equal(existsSync(shimPath), true);
|
|
749
|
+
const finalHooks = await readFile(hooksPath, 'utf-8');
|
|
750
|
+
assert.match(finalHooks, /FutureEvent/);
|
|
751
|
+
assert.match(finalHooks, /\\u005c/);
|
|
752
|
+
assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
await rm(wd, { recursive: true, force: true });
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
it('preserves a drive-qualified shim referenced by a drive-less future hook event', async () => {
|
|
760
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-drive-qualified-future-shim-'));
|
|
761
|
+
try {
|
|
762
|
+
await withDriveQualifiedWindowsCodexHome(wd, async (codexHomeDir) => {
|
|
763
|
+
const configPath = join(codexHomeDir, 'config.toml');
|
|
764
|
+
const hooksPath = join(codexHomeDir, 'hooks.json');
|
|
765
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
766
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
767
|
+
platform: 'win32',
|
|
768
|
+
codexHomeDir,
|
|
769
|
+
});
|
|
770
|
+
const driveLessFutureShimPath = '\\Users\\omx\\.codex\\hooks\\omx-native-hook-windows-shim.ps1';
|
|
771
|
+
const hooks = `${JSON.stringify({
|
|
772
|
+
...managed,
|
|
773
|
+
hooks: {
|
|
774
|
+
...managed.hooks,
|
|
775
|
+
FutureEvent: [{
|
|
776
|
+
hooks: [{
|
|
777
|
+
type: 'command',
|
|
778
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${driveLessFutureShimPath}'`,
|
|
779
|
+
}],
|
|
780
|
+
}],
|
|
781
|
+
},
|
|
782
|
+
}, null, 2)}\n`;
|
|
783
|
+
await writeFile(configPath, buildOmxConfig());
|
|
784
|
+
await writeFile(hooksPath, hooks);
|
|
785
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
786
|
+
await uninstall({ scope: 'user', transactionPlatform: 'win32' });
|
|
787
|
+
assert.equal(existsSync(shimPath), true);
|
|
788
|
+
const finalHooks = await readFile(hooksPath, 'utf-8');
|
|
789
|
+
assert.match(finalHooks, /FutureEvent/);
|
|
790
|
+
assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
await rm(wd, { recursive: true, force: true });
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
it('preserves a shim referenced through a normalized Windows path alias', async () => {
|
|
798
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-shim-path-alias-'));
|
|
799
|
+
try {
|
|
800
|
+
await withCwd(wd, async () => {
|
|
801
|
+
const codexDir = join(wd, '.codex');
|
|
802
|
+
const configPath = join(codexDir, 'config.toml');
|
|
803
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
804
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
805
|
+
const shimAlias = shimPath
|
|
806
|
+
.replace(/[\\/]+hooks[\\/]+/i, '\\hooks\\\\.\\')
|
|
807
|
+
.replace(/omx-native-hook-windows-shim\.ps1$/i, 'OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1')
|
|
808
|
+
.replace(/\\/g, '/');
|
|
809
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
810
|
+
platform: 'win32',
|
|
811
|
+
codexHomeDir: codexDir,
|
|
812
|
+
});
|
|
813
|
+
const hooks = `${JSON.stringify({
|
|
814
|
+
...managed,
|
|
815
|
+
hooks: {
|
|
816
|
+
...managed.hooks,
|
|
817
|
+
FutureEvent: [{
|
|
818
|
+
hooks: [{
|
|
819
|
+
type: 'command',
|
|
820
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`,
|
|
821
|
+
}],
|
|
822
|
+
}],
|
|
823
|
+
},
|
|
824
|
+
}, null, 2)}\n`;
|
|
825
|
+
await mkdir(codexDir, { recursive: true });
|
|
826
|
+
await writeFile(configPath, buildOmxConfig());
|
|
827
|
+
await writeFile(hooksPath, hooks);
|
|
828
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
829
|
+
await uninstall({ scope: 'project', transactionPlatform: 'win32' });
|
|
830
|
+
assert.equal(existsSync(shimPath), true);
|
|
831
|
+
assert.match(await readFile(hooksPath, 'utf-8'), /FutureEvent/);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
await rm(wd, { recursive: true, force: true });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
it('preserves a shim for a distinct absolute -File target', async () => {
|
|
839
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-quoted-inert-shim-'));
|
|
840
|
+
try {
|
|
841
|
+
await withCwd(wd, async () => {
|
|
842
|
+
const codexDir = join(wd, '.codex');
|
|
843
|
+
const configPath = join(codexDir, 'config.toml');
|
|
844
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
845
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
846
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
847
|
+
platform: 'win32',
|
|
848
|
+
codexHomeDir: codexDir,
|
|
849
|
+
});
|
|
850
|
+
const hooks = `${JSON.stringify({
|
|
851
|
+
...managed,
|
|
852
|
+
hooks: {
|
|
853
|
+
...managed.hooks,
|
|
854
|
+
FutureEvent: [{
|
|
855
|
+
hooks: [{
|
|
856
|
+
type: 'command',
|
|
857
|
+
command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
|
|
858
|
+
}],
|
|
859
|
+
}],
|
|
860
|
+
},
|
|
861
|
+
}, null, 2)}\n`;
|
|
862
|
+
await mkdir(codexDir, { recursive: true });
|
|
863
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
864
|
+
await writeFile(configPath, buildOmxConfig());
|
|
865
|
+
await writeFile(hooksPath, hooks);
|
|
866
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
867
|
+
await uninstall({ scope: 'project', transactionPlatform: 'win32' });
|
|
868
|
+
assert.equal(existsSync(shimPath), true);
|
|
869
|
+
assert.match(await readFile(hooksPath, 'utf-8'), /FutureEvent/);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
await rm(wd, { recursive: true, force: true });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
it('treats exact and all ambiguous preserved shim references as read-only transaction preconditions', async () => {
|
|
877
|
+
const fixtures = [
|
|
878
|
+
{ name: 'normalized-alias-deletion', reference: 'exact', mutation: 'delete' },
|
|
879
|
+
{ name: 'basename-ambiguity-replacement', reference: 'basename', mutation: 'replace' },
|
|
880
|
+
{ name: 'environment-indirection-replacement', reference: 'environment', mutation: 'replace' },
|
|
881
|
+
{ name: 'alternate-absolute-alias-deletion', reference: 'alternate-absolute', mutation: 'delete' },
|
|
882
|
+
{ name: 'rooted-short-name-deletion', reference: 'rooted-short-name', mutation: 'delete' },
|
|
883
|
+
{ name: 'drive-relative-short-name-deletion', reference: 'drive-relative-short-name', mutation: 'delete' },
|
|
884
|
+
{ name: 'distinct-basename-absolute-deletion', reference: 'distinct-basename-absolute', mutation: 'delete' },
|
|
885
|
+
];
|
|
886
|
+
for (const fixture of fixtures) {
|
|
887
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-preserved-shim-${fixture.name}-`));
|
|
888
|
+
try {
|
|
889
|
+
await withCwd(wd, async () => {
|
|
890
|
+
const codexDir = join(wd, '.codex');
|
|
891
|
+
const configPath = join(codexDir, 'config.toml');
|
|
892
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
893
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
894
|
+
const shimAlias = shimPath
|
|
895
|
+
.replace(/[\\/]+hooks[\\/]+/i, '\\hooks\\\\.\\')
|
|
896
|
+
.replace(/omx-native-hook-windows-shim\.ps1$/i, 'OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1')
|
|
897
|
+
.replace(/\\/g, '/');
|
|
898
|
+
const command = fixture.reference === 'exact'
|
|
899
|
+
? `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`
|
|
900
|
+
: fixture.reference === 'environment'
|
|
901
|
+
? '& $env:OMX_SHIM'
|
|
902
|
+
: fixture.reference === 'alternate-absolute'
|
|
903
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\aliases\\omx-native-hook-windows-shim.ps1'"
|
|
904
|
+
: fixture.reference === 'rooted-short-name'
|
|
905
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '\\Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1'"
|
|
906
|
+
: fixture.reference === 'drive-relative-short-name'
|
|
907
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'C:Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1'"
|
|
908
|
+
: fixture.reference === 'distinct-basename-absolute'
|
|
909
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\aliases\\foreign-hook-alias.ps1'"
|
|
910
|
+
: 'echo omx-native-hook-windows-shim.ps1';
|
|
911
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
912
|
+
platform: 'win32',
|
|
913
|
+
codexHomeDir: codexDir,
|
|
914
|
+
});
|
|
915
|
+
const hooks = `${JSON.stringify({
|
|
916
|
+
...managed,
|
|
917
|
+
hooks: {
|
|
918
|
+
...managed.hooks,
|
|
919
|
+
FutureEvent: [{ hooks: [{ type: 'command', command }] }],
|
|
379
920
|
},
|
|
380
|
-
|
|
921
|
+
}, null, 2)}\n`;
|
|
922
|
+
const config = buildOmxConfig();
|
|
923
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
924
|
+
const replacement = Buffer.from('concurrent shim replacement\n', 'utf-8');
|
|
925
|
+
await mkdir(codexDir, { recursive: true });
|
|
926
|
+
await writeFile(configPath, config);
|
|
927
|
+
await writeFile(hooksPath, hooks);
|
|
928
|
+
await writeFile(shimPath, shim);
|
|
929
|
+
await assert.rejects(uninstall({
|
|
930
|
+
scope: 'project',
|
|
931
|
+
transactionPlatform: 'win32',
|
|
932
|
+
transactionFailureInjector: async (stage) => {
|
|
933
|
+
if (stage !== 'before-config-commit')
|
|
934
|
+
return;
|
|
935
|
+
if (fixture.mutation === 'delete') {
|
|
936
|
+
await rm(shimPath);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
await writeFile(shimPath, replacement);
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
943
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, 'utf-8'));
|
|
944
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, 'utf-8'));
|
|
945
|
+
if (fixture.mutation === 'delete') {
|
|
946
|
+
assert.equal(existsSync(shimPath), false);
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
assert.deepEqual(await readFile(shimPath), replacement);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
await rm(wd, { recursive: true, force: true });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
it('rolls back hooks when a preserved environment-indirected shim drifts under --keep-config', async () => {
|
|
959
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-keep-config-preserved-shim-'));
|
|
960
|
+
try {
|
|
961
|
+
await withCwd(wd, async () => {
|
|
962
|
+
const codexDir = join(wd, '.codex');
|
|
963
|
+
const configPath = join(codexDir, 'config.toml');
|
|
964
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
965
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
966
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
967
|
+
platform: 'win32',
|
|
968
|
+
codexHomeDir: codexDir,
|
|
969
|
+
});
|
|
970
|
+
const hooks = `${JSON.stringify({
|
|
971
|
+
...managed,
|
|
972
|
+
hooks: {
|
|
973
|
+
...managed.hooks,
|
|
974
|
+
FutureEvent: [{ hooks: [{ type: 'command', command: '& $env:OMX_SHIM' }] }],
|
|
975
|
+
},
|
|
976
|
+
}, null, 2)}\n`;
|
|
977
|
+
const config = buildOmxConfig();
|
|
978
|
+
const replacement = Buffer.from('concurrent preserved shim replacement\n', 'utf-8');
|
|
979
|
+
await mkdir(codexDir, { recursive: true });
|
|
980
|
+
await writeFile(configPath, config);
|
|
981
|
+
await writeFile(hooksPath, hooks);
|
|
982
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
983
|
+
let injected = false;
|
|
984
|
+
await assert.rejects(uninstall({
|
|
985
|
+
scope: 'project',
|
|
986
|
+
keepConfig: true,
|
|
987
|
+
transactionPlatform: 'win32',
|
|
988
|
+
transactionFailureInjector: async (stage) => {
|
|
989
|
+
if (stage !== 'before-rename' || injected)
|
|
990
|
+
return;
|
|
991
|
+
injected = true;
|
|
992
|
+
await writeFile(shimPath, replacement);
|
|
993
|
+
},
|
|
994
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
995
|
+
assert.equal(injected, true);
|
|
996
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
997
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
998
|
+
assert.deepEqual(await readFile(shimPath), replacement);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
finally {
|
|
1002
|
+
await rm(wd, { recursive: true, force: true });
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
it('rolls back when a preserved Windows shim drifts after staged-cleanup finalization', async () => {
|
|
1006
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-preserved-shim-finalization-'));
|
|
1007
|
+
try {
|
|
1008
|
+
await withCwd(wd, async () => {
|
|
1009
|
+
const codexDir = join(wd, '.codex');
|
|
1010
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1011
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1012
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1013
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
1014
|
+
platform: 'win32',
|
|
1015
|
+
codexHomeDir: codexDir,
|
|
1016
|
+
});
|
|
1017
|
+
const hooks = `${JSON.stringify({
|
|
1018
|
+
...managed,
|
|
1019
|
+
hooks: {
|
|
1020
|
+
...managed.hooks,
|
|
1021
|
+
FutureEvent: [{ hooks: [{ type: 'command', command: '& $env:OMX_SHIM' }] }],
|
|
1022
|
+
},
|
|
1023
|
+
}, null, 2)}\n`;
|
|
1024
|
+
const config = buildOmxConfig();
|
|
1025
|
+
const replacement = Buffer.from('concurrent finalization shim replacement\n', 'utf-8');
|
|
1026
|
+
await mkdir(codexDir, { recursive: true });
|
|
1027
|
+
await writeFile(configPath, config);
|
|
1028
|
+
await writeFile(hooksPath, hooks);
|
|
1029
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
1030
|
+
let injected = false;
|
|
1031
|
+
await assert.rejects(uninstall({
|
|
1032
|
+
scope: 'project',
|
|
1033
|
+
transactionPlatform: 'win32',
|
|
1034
|
+
transactionFailureInjector: async (stage) => {
|
|
1035
|
+
if (stage !== 'after-staged-cleanup' || injected)
|
|
1036
|
+
return;
|
|
1037
|
+
injected = true;
|
|
1038
|
+
await writeFile(shimPath, replacement);
|
|
1039
|
+
},
|
|
1040
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1041
|
+
assert.equal(injected, true);
|
|
1042
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1043
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1044
|
+
assert.deepEqual(await readFile(shimPath), replacement);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
finally {
|
|
1048
|
+
await rm(wd, { recursive: true, force: true });
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
it('rolls back hooks, historical proof-owned shim, and config when shim removal fails', async () => {
|
|
1052
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-shim-rollback-'));
|
|
1053
|
+
try {
|
|
1054
|
+
await withCwd(wd, async () => {
|
|
1055
|
+
const codexDir = join(wd, '.codex');
|
|
1056
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1057
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1058
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1059
|
+
const config = buildOmxConfig();
|
|
1060
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1061
|
+
platform: 'win32',
|
|
1062
|
+
codexHomeDir: codexDir,
|
|
1063
|
+
}), null, 2)}\n`;
|
|
1064
|
+
const shim = buildManagedCodexNativeHookWindowsShimContent('C:\\Historical Install\\oh-my-codex', { nodePath: 'D:\\Historical Node\\node.exe' });
|
|
1065
|
+
await mkdir(codexDir, { recursive: true });
|
|
1066
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1067
|
+
await writeFile(configPath, config);
|
|
1068
|
+
await writeFile(hooksPath, hooks);
|
|
1069
|
+
await writeFile(shimPath, shim);
|
|
1070
|
+
await assert.rejects(uninstall({
|
|
1071
|
+
scope: 'project',
|
|
1072
|
+
transactionPlatform: 'win32',
|
|
1073
|
+
transactionFailureInjector: async (stage) => {
|
|
1074
|
+
if (stage !== 'before-shim-removal')
|
|
1075
|
+
return;
|
|
1076
|
+
assert.equal(existsSync(hooksPath), false, 'hooks must mutate before shim removal');
|
|
1077
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1078
|
+
throw new Error('simulated shim removal failure');
|
|
1079
|
+
},
|
|
1080
|
+
}), /simulated shim removal failure/);
|
|
1081
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1082
|
+
assert.equal(await readFile(shimPath, 'utf-8'), shim);
|
|
1083
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
await rm(wd, { recursive: true, force: true });
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
it('rejects a modified Windows shim before any uninstall artifact write', async () => {
|
|
1091
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-modified-shim-'));
|
|
1092
|
+
try {
|
|
1093
|
+
await withCwd(wd, async () => {
|
|
1094
|
+
const codexDir = join(wd, '.codex');
|
|
1095
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1096
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1097
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1098
|
+
const config = buildOmxConfig();
|
|
1099
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1100
|
+
platform: 'win32',
|
|
1101
|
+
codexHomeDir: codexDir,
|
|
1102
|
+
}), null, 2)}\n`;
|
|
1103
|
+
const shim = `${buildManagedCodexNativeHookWindowsShimContent(packageRoot())}\n# user edit\n`;
|
|
1104
|
+
await mkdir(codexDir, { recursive: true });
|
|
1105
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1106
|
+
await writeFile(configPath, config);
|
|
1107
|
+
await writeFile(hooksPath, hooks);
|
|
1108
|
+
await writeFile(shimPath, shim);
|
|
1109
|
+
await assert.rejects(uninstall({ scope: 'project', transactionPlatform: 'win32' }), /modified native hook Windows shim/);
|
|
1110
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1111
|
+
assert.equal(await readFile(shimPath, 'utf-8'), shim);
|
|
1112
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
await rm(wd, { recursive: true, force: true });
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
it('rolls back hooks, shim, and config when config commit fails after hook mutation', async () => {
|
|
1120
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-config-rollback-'));
|
|
1121
|
+
try {
|
|
1122
|
+
await withCwd(wd, async () => {
|
|
1123
|
+
const codexDir = join(wd, '.codex');
|
|
1124
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1125
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1126
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1127
|
+
const config = buildOmxConfig();
|
|
1128
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1129
|
+
platform: 'win32',
|
|
1130
|
+
codexHomeDir: codexDir,
|
|
1131
|
+
}), null, 2)}\n`;
|
|
1132
|
+
const shim = buildManagedCodexNativeHookWindowsShimContent(packageRoot());
|
|
1133
|
+
await mkdir(codexDir, { recursive: true });
|
|
1134
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1135
|
+
await writeFile(configPath, config);
|
|
1136
|
+
await writeFile(hooksPath, hooks);
|
|
1137
|
+
await writeFile(shimPath, shim);
|
|
1138
|
+
await assert.rejects(uninstall({
|
|
1139
|
+
scope: 'project',
|
|
1140
|
+
transactionPlatform: 'win32',
|
|
1141
|
+
transactionFailureInjector: async (stage) => {
|
|
1142
|
+
if (stage !== 'after-config-commit')
|
|
1143
|
+
return;
|
|
1144
|
+
assert.equal(existsSync(hooksPath), false, 'hooks must mutate before config commit');
|
|
1145
|
+
assert.equal(existsSync(shimPath), false, 'shim must mutate before config commit');
|
|
1146
|
+
assert.notEqual(await readFile(configPath, 'utf-8'), config);
|
|
1147
|
+
throw new Error('simulated config commit failure');
|
|
1148
|
+
},
|
|
1149
|
+
}), /simulated config commit failure/);
|
|
1150
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1151
|
+
assert.equal(await readFile(shimPath, 'utf-8'), shim);
|
|
1152
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
finally {
|
|
1156
|
+
await rm(wd, { recursive: true, force: true });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
it('rejects invalid UTF-8 hooks.json and config.toml before changing either artifact', async () => {
|
|
1160
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-invalid-utf8-'));
|
|
1161
|
+
try {
|
|
1162
|
+
await withCwd(wd, async () => {
|
|
1163
|
+
const codexDir = join(wd, '.codex');
|
|
1164
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1165
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1166
|
+
const hooks = Buffer.from([0x7b, 0xff, 0x7d]);
|
|
1167
|
+
const config = Buffer.from([0x6f, 0x6d, 0x78, 0x5f, 0xff]);
|
|
1168
|
+
await mkdir(codexDir, { recursive: true });
|
|
1169
|
+
await writeFile(configPath, buildOmxConfig());
|
|
1170
|
+
await writeFile(hooksPath, hooks);
|
|
1171
|
+
await assert.rejects(uninstall({ scope: 'project' }), /not valid UTF-8/);
|
|
1172
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1173
|
+
await writeFile(hooksPath, JSON.stringify(buildManagedCodexHooksConfig(packageRoot())));
|
|
1174
|
+
await writeFile(configPath, config);
|
|
1175
|
+
await assert.rejects(uninstall({ scope: 'project' }), /not valid UTF-8/);
|
|
1176
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1177
|
+
const bomHooks = Buffer.concat([
|
|
1178
|
+
Buffer.from([0xef, 0xbb, 0xbf]),
|
|
1179
|
+
Buffer.from('{"hooks":{}}\n', 'utf-8'),
|
|
1180
|
+
]);
|
|
1181
|
+
await writeFile(configPath, buildOmxConfig());
|
|
1182
|
+
await writeFile(hooksPath, bomHooks);
|
|
1183
|
+
await assert.rejects(uninstall({ scope: 'project' }), /must contain a JSON object/);
|
|
1184
|
+
assert.deepEqual(await readFile(hooksPath), bomHooks);
|
|
1185
|
+
assert.equal(existsSync(hooksPath), true);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
finally {
|
|
1189
|
+
await rm(wd, { recursive: true, force: true });
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
it('rejects symlinked native hook artifacts without following their targets', async () => {
|
|
1193
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-symlink-hooks-'));
|
|
1194
|
+
try {
|
|
1195
|
+
await withCwd(wd, async () => {
|
|
1196
|
+
const codexDir = join(wd, '.codex');
|
|
1197
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1198
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1199
|
+
const targetPath = join(wd, 'user-hooks.json');
|
|
1200
|
+
const config = buildOmxConfig();
|
|
1201
|
+
const target = '{"user":"hooks"}\n';
|
|
1202
|
+
await mkdir(codexDir, { recursive: true });
|
|
1203
|
+
await writeFile(configPath, config);
|
|
1204
|
+
await writeFile(targetPath, target);
|
|
1205
|
+
await symlink(targetPath, hooksPath);
|
|
1206
|
+
await assert.rejects(uninstall({ scope: 'project' }), /expected a regular file, not a symbolic link/);
|
|
1207
|
+
assert.equal((await lstat(hooksPath)).isSymbolicLink(), true);
|
|
1208
|
+
assert.equal(await readFile(targetPath, 'utf-8'), target);
|
|
1209
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
finally {
|
|
1213
|
+
await rm(wd, { recursive: true, force: true });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
it('aborts before the first removal when any planned artifact becomes stale', async () => {
|
|
1217
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-snapshot-'));
|
|
1218
|
+
try {
|
|
1219
|
+
await withCwd(wd, async () => {
|
|
1220
|
+
const codexDir = join(wd, '.codex');
|
|
1221
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1222
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1223
|
+
const config = buildOmxConfig();
|
|
1224
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1225
|
+
const staleConfig = `${config}# concurrent user edit\n`;
|
|
1226
|
+
await mkdir(codexDir, { recursive: true });
|
|
1227
|
+
await writeFile(configPath, config);
|
|
1228
|
+
await writeFile(hooksPath, hooks);
|
|
1229
|
+
await assert.rejects(uninstall({
|
|
1230
|
+
scope: 'project',
|
|
1231
|
+
transactionFailureInjector: async (stage) => {
|
|
1232
|
+
if (stage === 'before-hooks-commit')
|
|
1233
|
+
await writeFile(configPath, staleConfig);
|
|
1234
|
+
},
|
|
1235
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1236
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1237
|
+
assert.equal(await readFile(configPath, 'utf-8'), staleConfig);
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
finally {
|
|
1241
|
+
await rm(wd, { recursive: true, force: true });
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
it('never replaces a concurrent symlink while rolling back a failed uninstall transaction', async () => {
|
|
1245
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-rollback-link-'));
|
|
1246
|
+
try {
|
|
1247
|
+
await withCwd(wd, async () => {
|
|
1248
|
+
const codexDir = join(wd, '.codex');
|
|
1249
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1250
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1251
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1252
|
+
const replacementPath = join(wd, 'concurrent-user-hooks.json');
|
|
1253
|
+
const config = buildOmxConfig();
|
|
1254
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1255
|
+
platform: 'win32',
|
|
1256
|
+
codexHomeDir: codexDir,
|
|
1257
|
+
}), null, 2)}\n`;
|
|
1258
|
+
const replacement = '{"user":"replacement"}\n';
|
|
1259
|
+
await mkdir(codexDir, { recursive: true });
|
|
1260
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1261
|
+
await writeFile(configPath, config);
|
|
1262
|
+
await writeFile(hooksPath, hooks);
|
|
1263
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
1264
|
+
await writeFile(replacementPath, replacement);
|
|
1265
|
+
await assert.rejects(uninstall({
|
|
1266
|
+
scope: 'project',
|
|
1267
|
+
transactionPlatform: 'win32',
|
|
1268
|
+
transactionFailureInjector: async (stage) => {
|
|
1269
|
+
if (stage !== 'before-shim-removal')
|
|
1270
|
+
return;
|
|
1271
|
+
await symlink(replacementPath, hooksPath);
|
|
1272
|
+
throw new Error('simulated concurrent hook replacement');
|
|
1273
|
+
},
|
|
1274
|
+
}), /Uninstall artifact rollback failed.*(?:Refusing stale rollback|expected a regular file, not a symbolic link)/);
|
|
1275
|
+
assert.equal((await lstat(hooksPath)).isSymbolicLink(), true);
|
|
1276
|
+
assert.equal(await readFile(replacementPath, 'utf-8'), replacement);
|
|
1277
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
await rm(wd, { recursive: true, force: true });
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
it('rejects a symlinked controlled ancestor before reading an escaped artifact', async () => {
|
|
1285
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-ancestor-link-'));
|
|
1286
|
+
try {
|
|
1287
|
+
await withCwd(wd, async () => {
|
|
1288
|
+
const codexDir = join(wd, '.codex');
|
|
1289
|
+
const escapedDir = join(wd, 'escaped-codex');
|
|
1290
|
+
const configPath = join(escapedDir, 'config.toml');
|
|
1291
|
+
const config = buildOmxConfig();
|
|
1292
|
+
await mkdir(escapedDir, { recursive: true });
|
|
1293
|
+
await writeFile(configPath, config);
|
|
1294
|
+
await symlink(escapedDir, codexDir, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1295
|
+
await assert.rejects(uninstall({ scope: 'project' }), /controlled ancestor .* must be a non-symbolic-link directory/);
|
|
1296
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
finally {
|
|
1300
|
+
await rm(wd, { recursive: true, force: true });
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
it('does not truncate or follow regular and symlink replacement temporary collisions', async () => {
|
|
1304
|
+
for (const collisionKind of ['regular', 'symlink']) {
|
|
1305
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-temp-${collisionKind}-`));
|
|
1306
|
+
try {
|
|
1307
|
+
await withCwd(wd, async () => {
|
|
1308
|
+
const codexDir = join(wd, '.codex');
|
|
1309
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1310
|
+
const collisionPath = join(codexDir, '.config.toml.collision');
|
|
1311
|
+
const collisionTarget = join(wd, 'collision-target');
|
|
1312
|
+
const config = buildOmxConfig();
|
|
1313
|
+
const collision = 'foreign temporary collision\n';
|
|
1314
|
+
await mkdir(codexDir, { recursive: true });
|
|
1315
|
+
await writeFile(configPath, config);
|
|
1316
|
+
if (collisionKind === 'regular') {
|
|
1317
|
+
await writeFile(collisionPath, collision);
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
await writeFile(collisionTarget, collision);
|
|
1321
|
+
await symlink(collisionTarget, collisionPath);
|
|
1322
|
+
}
|
|
1323
|
+
await assert.rejects(uninstall({
|
|
1324
|
+
scope: 'project',
|
|
1325
|
+
transactionTemporaryPath: () => collisionPath,
|
|
1326
|
+
}), /EEXIST/);
|
|
1327
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1328
|
+
assert.equal(await readFile(collisionKind === 'regular' ? collisionPath : collisionTarget, 'utf-8'), collision);
|
|
1329
|
+
if (collisionKind === 'symlink') {
|
|
1330
|
+
assert.equal((await lstat(collisionPath)).isSymbolicLink(), true);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
finally {
|
|
1335
|
+
await rm(wd, { recursive: true, force: true });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
it('rejects stale snapshots immediately before forward rename and remove mutations', async () => {
|
|
1340
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-forward-'));
|
|
1341
|
+
try {
|
|
1342
|
+
await withCwd(wd, async () => {
|
|
1343
|
+
const codexDir = join(wd, '.codex');
|
|
1344
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1345
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1346
|
+
const config = buildOmxConfig();
|
|
1347
|
+
const concurrentConfig = `${config}# concurrent replacement\n`;
|
|
1348
|
+
await mkdir(codexDir, { recursive: true });
|
|
1349
|
+
await writeFile(configPath, config);
|
|
1350
|
+
await assert.rejects(uninstall({
|
|
1351
|
+
scope: 'project',
|
|
1352
|
+
transactionFailureInjector: async (stage) => {
|
|
1353
|
+
if (stage === 'before-rename')
|
|
1354
|
+
await writeFile(configPath, concurrentConfig);
|
|
1355
|
+
},
|
|
1356
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1357
|
+
assert.equal(await readFile(configPath, 'utf-8'), concurrentConfig);
|
|
1358
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1359
|
+
const concurrentHooks = '{"foreign":"replacement"}\n';
|
|
1360
|
+
await writeFile(configPath, config);
|
|
1361
|
+
await writeFile(hooksPath, hooks);
|
|
1362
|
+
await assert.rejects(uninstall({
|
|
1363
|
+
scope: 'project',
|
|
1364
|
+
transactionFailureInjector: async (stage) => {
|
|
1365
|
+
if (stage === 'before-remove')
|
|
1366
|
+
await writeFile(hooksPath, concurrentHooks);
|
|
1367
|
+
},
|
|
1368
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1369
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), concurrentHooks);
|
|
1370
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
finally {
|
|
1374
|
+
await rm(wd, { recursive: true, force: true });
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
it('fails closed for non-throwing regular and symlink write-temporary replacements', async () => {
|
|
1378
|
+
for (const replacementKind of ['regular', 'symlink']) {
|
|
1379
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-write-temp-${replacementKind}-`));
|
|
1380
|
+
try {
|
|
1381
|
+
await withCwd(wd, async () => {
|
|
1382
|
+
const codexDir = join(wd, '.codex');
|
|
1383
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1384
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1385
|
+
const temporaryPath = join(codexDir, '.config.toml.write-temporary');
|
|
1386
|
+
const replacementTarget = join(wd, 'foreign-write-temporary');
|
|
1387
|
+
const config = Buffer.from(buildOmxConfig());
|
|
1388
|
+
const hooks = Buffer.from('{}\n');
|
|
1389
|
+
const replacement = Buffer.from('foreign write temporary replacement\n');
|
|
1390
|
+
await mkdir(codexDir, { recursive: true });
|
|
1391
|
+
await writeFile(configPath, config);
|
|
1392
|
+
await writeFile(hooksPath, hooks);
|
|
1393
|
+
await assert.rejects(uninstall({
|
|
1394
|
+
scope: 'project',
|
|
1395
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'write' ? temporaryPath : join(codexDir, '.hooks.json.staged'),
|
|
1396
|
+
transactionFailureInjector: async (stage) => {
|
|
1397
|
+
if (stage !== 'before-rename')
|
|
1398
|
+
return;
|
|
1399
|
+
await rm(temporaryPath, { force: true });
|
|
1400
|
+
if (replacementKind === 'regular') {
|
|
1401
|
+
await writeFile(temporaryPath, replacement);
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
await writeFile(replacementTarget, replacement);
|
|
1405
|
+
await symlink(replacementTarget, temporaryPath);
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
}), (error) => {
|
|
1409
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1410
|
+
assert.match(message, /planned artifact .* changed, was created, or was removed after planning|expected a regular file, not a symbolic link/);
|
|
1411
|
+
assert.ok(message.includes(temporaryPath));
|
|
1412
|
+
assert.match(message, /preserved temporary .*manual recovery after cleanup verification failed/);
|
|
1413
|
+
return true;
|
|
1414
|
+
});
|
|
1415
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1416
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1417
|
+
if (replacementKind === 'regular') {
|
|
1418
|
+
assert.deepEqual(await readFile(temporaryPath), replacement);
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
assert.equal((await lstat(temporaryPath)).isSymbolicLink(), true);
|
|
1422
|
+
assert.deepEqual(await readFile(replacementTarget), replacement);
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
finally {
|
|
1427
|
+
await rm(wd, { recursive: true, force: true });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
it('fails closed for non-throwing regular and symlink staged-tombstone replacements', async () => {
|
|
1432
|
+
for (const replacementKind of ['regular', 'symlink']) {
|
|
1433
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-staged-tombstone-${replacementKind}-`));
|
|
1434
|
+
try {
|
|
1435
|
+
await withCwd(wd, async () => {
|
|
1436
|
+
const codexDir = join(wd, '.codex');
|
|
1437
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1438
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1439
|
+
const stagedPath = join(codexDir, '.hooks.json.staged-tombstone');
|
|
1440
|
+
const replacementTarget = join(wd, 'foreign-staged-tombstone');
|
|
1441
|
+
const config = Buffer.from(buildOmxConfig());
|
|
1442
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`);
|
|
1443
|
+
const replacement = Buffer.from('foreign staged tombstone replacement\n');
|
|
1444
|
+
await mkdir(codexDir, { recursive: true });
|
|
1445
|
+
await writeFile(configPath, config);
|
|
1446
|
+
await writeFile(hooksPath, hooks);
|
|
1447
|
+
await assert.rejects(uninstall({
|
|
1448
|
+
scope: 'project',
|
|
1449
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1450
|
+
transactionFailureInjector: async (stage) => {
|
|
1451
|
+
if (stage !== 'before-remove')
|
|
1452
|
+
return;
|
|
1453
|
+
await rm(stagedPath, { force: true });
|
|
1454
|
+
if (replacementKind === 'regular') {
|
|
1455
|
+
await writeFile(stagedPath, replacement);
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
await writeFile(replacementTarget, replacement);
|
|
1459
|
+
await symlink(replacementTarget, stagedPath);
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
}), (error) => {
|
|
1463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1464
|
+
assert.match(message, /planned artifact .* changed, was created, or was removed after planning|expected a regular file, not a symbolic link/);
|
|
1465
|
+
assert.ok(message.includes(stagedPath));
|
|
1466
|
+
assert.match(message, /preserved staged deletion .*manual recovery after cleanup verification failed/);
|
|
1467
|
+
return true;
|
|
1468
|
+
});
|
|
1469
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1470
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1471
|
+
if (replacementKind === 'regular') {
|
|
1472
|
+
assert.deepEqual(await readFile(stagedPath), replacement);
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
assert.equal((await lstat(stagedPath)).isSymbolicLink(), true);
|
|
1476
|
+
assert.deepEqual(await readFile(replacementTarget), replacement);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
finally {
|
|
1481
|
+
await rm(wd, { recursive: true, force: true });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
it('rejects stale snapshots immediately before rollback rename and staged-copy removal', async () => {
|
|
1486
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-rollback-primitives-'));
|
|
1487
|
+
try {
|
|
1488
|
+
await withCwd(wd, async () => {
|
|
1489
|
+
const codexDir = join(wd, '.codex');
|
|
1490
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1491
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1492
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1493
|
+
const config = buildOmxConfig();
|
|
1494
|
+
await mkdir(codexDir, { recursive: true });
|
|
1495
|
+
await writeFile(configPath, config);
|
|
1496
|
+
await writeFile(hooksPath, hooks);
|
|
1497
|
+
const concurrentHooks = '{"foreign":"rollback rename"}\n';
|
|
1498
|
+
await assert.rejects(uninstall({
|
|
1499
|
+
scope: 'project',
|
|
1500
|
+
transactionFailureInjector: async (stage) => {
|
|
1501
|
+
if (stage === 'before-config-commit')
|
|
1502
|
+
throw new Error('stop after hook deletion');
|
|
1503
|
+
if (stage === 'before-rollback-rename') {
|
|
1504
|
+
await writeFile(hooksPath, concurrentHooks);
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
}), /Uninstall artifact rollback failed.*Refusing uninstall because planned artifact/);
|
|
1508
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), concurrentHooks);
|
|
1509
|
+
await writeFile(hooksPath, hooks);
|
|
1510
|
+
const stagedPath = join(codexDir, '.hooks.json.rollback-stage');
|
|
1511
|
+
const staleStagedCopy = 'foreign staged copy\n';
|
|
1512
|
+
await assert.rejects(uninstall({
|
|
1513
|
+
scope: 'project',
|
|
1514
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1515
|
+
transactionFailureInjector: async (stage) => {
|
|
1516
|
+
if (stage === 'before-config-commit')
|
|
1517
|
+
throw new Error('stop after hook deletion');
|
|
1518
|
+
if (stage === 'before-rollback-remove') {
|
|
1519
|
+
await writeFile(stagedPath, staleStagedCopy);
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
}), /Uninstall artifact rollback failed.*staged deletion cleanup/);
|
|
1523
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1524
|
+
assert.equal(await readFile(stagedPath, 'utf-8'), staleStagedCopy);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
finally {
|
|
1528
|
+
await rm(wd, { recursive: true, force: true });
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
it('rolls back when staged-deletion cleanup fails before any staged copy is committed', async () => {
|
|
1532
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-staged-deletion-'));
|
|
1533
|
+
try {
|
|
1534
|
+
await withCwd(wd, async () => {
|
|
1535
|
+
const codexDir = join(wd, '.codex');
|
|
1536
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1537
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1538
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1539
|
+
const config = buildOmxConfig();
|
|
1540
|
+
const stagedPath = join(codexDir, '.hooks.json.staged');
|
|
1541
|
+
await mkdir(codexDir, { recursive: true });
|
|
1542
|
+
await writeFile(configPath, config);
|
|
1543
|
+
await writeFile(hooksPath, hooks);
|
|
1544
|
+
await uninstall({
|
|
1545
|
+
scope: 'project',
|
|
1546
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1547
|
+
});
|
|
1548
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1549
|
+
assert.equal(existsSync(stagedPath), false, 'successful commit leaves no staged tombstone');
|
|
1550
|
+
await writeFile(configPath, config);
|
|
1551
|
+
await writeFile(hooksPath, hooks);
|
|
1552
|
+
let interrupted = false;
|
|
1553
|
+
await assert.rejects(uninstall({
|
|
1554
|
+
scope: 'project',
|
|
1555
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1556
|
+
transactionFailureInjector: (stage) => {
|
|
1557
|
+
if (stage === 'before-staged-cleanup' && !interrupted) {
|
|
1558
|
+
interrupted = true;
|
|
1559
|
+
throw new Error('staged deletion cleanup interrupted');
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
}), /staged deletion cleanup interrupted/);
|
|
1563
|
+
assert.equal(interrupted, true);
|
|
1564
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1565
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1566
|
+
assert.equal(existsSync(stagedPath), false);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
finally {
|
|
1570
|
+
await rm(wd, { recursive: true, force: true });
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
it('does not roll back when preserved metadata drifts after staged-deletion cleanup finalization', async () => {
|
|
1574
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-second-staged-drift-'));
|
|
1575
|
+
try {
|
|
1576
|
+
await withCwd(wd, async () => {
|
|
1577
|
+
const codexDir = join(wd, '.codex');
|
|
1578
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1579
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1580
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1581
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
1582
|
+
const dispatcherPath = join(packageRoot(), 'dist', 'scripts', 'notify-dispatcher.js');
|
|
1583
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1584
|
+
platform: 'win32',
|
|
1585
|
+
codexHomeDir: codexDir,
|
|
1586
|
+
}), null, 2)}\n`;
|
|
1587
|
+
const metadata = {
|
|
1588
|
+
managedBy: 'oh-my-codex',
|
|
1589
|
+
version: 1,
|
|
1590
|
+
previousNotify: ['node', '/tmp/user-notify.js'],
|
|
1591
|
+
omxNotify: ['node', join(packageRoot(), 'dist', 'scripts', 'notify-hook.js')],
|
|
1592
|
+
dispatcherNotify: ['node', dispatcherPath, '--metadata', metadataPath],
|
|
1593
|
+
};
|
|
1594
|
+
const foreignMetadata = JSON.stringify({
|
|
1595
|
+
...metadata,
|
|
1596
|
+
previousNotify: ['node', '/tmp/concurrent-notify.js'],
|
|
1597
|
+
});
|
|
1598
|
+
const config = buildOmxConfig().replace(/^notify = .*$/m, `notify = ${JSON.stringify(metadata.dispatcherNotify)}`);
|
|
1599
|
+
const stagedDeletionPath = (path, purpose) => join(dirname(path), `.${basename(path)}.cleanup-${purpose}`);
|
|
1600
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1601
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1602
|
+
await writeFile(configPath, config);
|
|
1603
|
+
await writeFile(hooksPath, hooks);
|
|
1604
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
1605
|
+
await writeFile(metadataPath, JSON.stringify(metadata));
|
|
1606
|
+
let injected = false;
|
|
1607
|
+
await assert.rejects(uninstall({
|
|
1608
|
+
scope: 'project',
|
|
1609
|
+
transactionPlatform: 'win32',
|
|
1610
|
+
transactionTemporaryPath: stagedDeletionPath,
|
|
1611
|
+
transactionFailureInjector: async (stage) => {
|
|
1612
|
+
if (stage !== 'after-staged-cleanup' || injected)
|
|
1613
|
+
return;
|
|
1614
|
+
injected = true;
|
|
1615
|
+
await writeFile(metadataPath, foreignMetadata);
|
|
1616
|
+
},
|
|
1617
|
+
}), /committed but staged deletion cleanup failed/);
|
|
1618
|
+
assert.equal(injected, true);
|
|
1619
|
+
assert.equal(await readFile(metadataPath, 'utf-8'), foreignMetadata);
|
|
1620
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1621
|
+
assert.equal(existsSync(shimPath), false);
|
|
1622
|
+
assert.notEqual(await readFile(configPath, 'utf-8'), config);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
finally {
|
|
1626
|
+
await rm(wd, { recursive: true, force: true });
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
it('treats applied hook, shim, and config snapshots as CAS through commit finalization', async () => {
|
|
1630
|
+
const fixtures = [
|
|
1631
|
+
{ name: 'later commit', stage: 'before-shim-removal', drift: 'hooks', committed: false },
|
|
1632
|
+
{ name: 'staged cleanup', stage: 'before-staged-cleanup', drift: 'shim', committed: false },
|
|
1633
|
+
{ name: 'finalization', stage: 'after-staged-cleanup', drift: 'config', committed: true },
|
|
1634
|
+
];
|
|
1635
|
+
for (const fixture of fixtures) {
|
|
1636
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-applied-${fixture.name}-`));
|
|
1637
|
+
try {
|
|
1638
|
+
await withCwd(wd, async () => {
|
|
1639
|
+
const codexDir = join(wd, '.codex');
|
|
1640
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1641
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1642
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1643
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
1644
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1645
|
+
platform: 'win32',
|
|
1646
|
+
codexHomeDir: codexDir,
|
|
1647
|
+
}), null, 2)}\n`, 'utf-8');
|
|
1648
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
1649
|
+
const paths = { config: configPath, hooks: hooksPath, shim: shimPath };
|
|
1650
|
+
const before = { config, hooks, shim };
|
|
1651
|
+
const foreign = Buffer.from(`foreign applied ${fixture.drift} ${fixture.name}\n`, 'utf-8');
|
|
1652
|
+
await mkdir(codexDir, { recursive: true });
|
|
1653
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1654
|
+
await writeFile(configPath, config);
|
|
1655
|
+
await writeFile(hooksPath, hooks);
|
|
1656
|
+
await writeFile(shimPath, shim);
|
|
1657
|
+
let injected = false;
|
|
1658
|
+
await assert.rejects(uninstall({
|
|
1659
|
+
scope: 'project',
|
|
1660
|
+
transactionPlatform: 'win32',
|
|
1661
|
+
transactionFailureInjector: async (stage) => {
|
|
1662
|
+
if (stage !== fixture.stage || injected)
|
|
1663
|
+
return;
|
|
1664
|
+
injected = true;
|
|
1665
|
+
await writeFile(paths[fixture.drift], foreign);
|
|
1666
|
+
},
|
|
1667
|
+
}), fixture.committed
|
|
1668
|
+
? /committed but staged deletion cleanup failed during finalization/
|
|
1669
|
+
: /Uninstall artifact rollback failed/);
|
|
1670
|
+
assert.equal(injected, true, fixture.name);
|
|
1671
|
+
if (fixture.name === 'staged cleanup') {
|
|
1672
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1673
|
+
assert.deepEqual(await readFile(shimPath), foreign);
|
|
1674
|
+
assert.notDeepEqual(await readFile(configPath), before.config);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (fixture.committed) {
|
|
1678
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1679
|
+
assert.equal(existsSync(shimPath), false);
|
|
1680
|
+
assert.deepEqual(await readFile(configPath), foreign);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
assert.deepEqual(await readFile(hooksPath), fixture.drift === 'hooks' ? foreign : before.hooks);
|
|
1684
|
+
assert.deepEqual(await readFile(shimPath), before.shim);
|
|
1685
|
+
assert.deepEqual(await readFile(configPath), before.config);
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
finally {
|
|
1689
|
+
await rm(wd, { recursive: true, force: true });
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
it('rolls back registered destinations when post-rename or post-remove verification fails', async () => {
|
|
1694
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-post-apply-recovery-'));
|
|
1695
|
+
try {
|
|
1696
|
+
await withCwd(wd, async () => {
|
|
1697
|
+
const codexDir = join(wd, '.codex');
|
|
1698
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1699
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1700
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
1701
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`, 'utf-8');
|
|
1702
|
+
await mkdir(codexDir, { recursive: true });
|
|
1703
|
+
await writeFile(configPath, config);
|
|
1704
|
+
await assert.rejects(uninstall({
|
|
1705
|
+
scope: 'project',
|
|
1706
|
+
transactionFailureInjector: (stage) => {
|
|
1707
|
+
if (stage === 'after-rename')
|
|
1708
|
+
throw new Error('post-rename verification failure');
|
|
1709
|
+
},
|
|
1710
|
+
}), /post-rename verification failure/);
|
|
1711
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1712
|
+
await writeFile(configPath, config);
|
|
1713
|
+
await writeFile(hooksPath, hooks);
|
|
1714
|
+
await assert.rejects(uninstall({
|
|
1715
|
+
scope: 'project',
|
|
1716
|
+
transactionFailureInjector: (stage) => {
|
|
1717
|
+
if (stage === 'after-remove')
|
|
1718
|
+
throw new Error('post-remove verification failure');
|
|
1719
|
+
},
|
|
1720
|
+
}), /post-remove verification failure/);
|
|
1721
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1722
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
finally {
|
|
1726
|
+
await rm(wd, { recursive: true, force: true });
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
it('strictly validates dispatcher metadata and treats it as a stale read-only transaction precondition', async () => {
|
|
1730
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-notify-metadata-'));
|
|
1731
|
+
try {
|
|
1732
|
+
await withCwd(wd, async () => {
|
|
1733
|
+
const codexDir = join(wd, '.codex');
|
|
1734
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1735
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
1736
|
+
const dispatcherPath = join(packageRoot(), 'dist', 'scripts', 'notify-dispatcher.js');
|
|
1737
|
+
const config = buildOmxConfig().replace(/^notify = .*$/m, `notify = ${JSON.stringify(['node', dispatcherPath, '--metadata', metadataPath])}`);
|
|
1738
|
+
const metadata = {
|
|
1739
|
+
managedBy: 'oh-my-codex',
|
|
1740
|
+
version: 1,
|
|
1741
|
+
previousNotify: ['node', '/tmp/user-notify.js'],
|
|
1742
|
+
omxNotify: ['node', join(packageRoot(), 'dist', 'scripts', 'notify-hook.js')],
|
|
1743
|
+
dispatcherNotify: ['node', dispatcherPath, '--metadata', metadataPath],
|
|
1744
|
+
};
|
|
1745
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1746
|
+
await writeFile(configPath, config);
|
|
1747
|
+
await writeFile(metadataPath, JSON.stringify({ ...metadata, previousNotify: ['not-a-string', 7] }));
|
|
1748
|
+
await assert.rejects(uninstall({ scope: 'project' }), /previousNotify must be null or an array of strings/);
|
|
1749
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1750
|
+
await writeFile(metadataPath, Buffer.from([0x7b, 0xff, 0x7d]));
|
|
1751
|
+
await assert.rejects(uninstall({ scope: 'project' }), /not valid UTF-8/);
|
|
1752
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1753
|
+
await writeFile(metadataPath, JSON.stringify(metadata));
|
|
1754
|
+
await assert.rejects(uninstall({
|
|
1755
|
+
scope: 'project',
|
|
1756
|
+
transactionFailureInjector: async (stage) => {
|
|
1757
|
+
if (stage === 'before-config-commit') {
|
|
1758
|
+
await writeFile(metadataPath, JSON.stringify({ ...metadata, previousNotify: ['node', '/tmp/concurrent.js'] }));
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1762
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
finally {
|
|
1766
|
+
await rm(wd, { recursive: true, force: true });
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
it('does not report stale rollback recovery when failure occurs before destructive removal', async () => {
|
|
1770
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-pre-destructive-'));
|
|
1771
|
+
try {
|
|
1772
|
+
await withCwd(wd, async () => {
|
|
1773
|
+
const codexDir = join(wd, '.codex');
|
|
1774
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1775
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1776
|
+
const stagedPath = join(codexDir, '.hooks.json.pre-destructive');
|
|
1777
|
+
const config = buildOmxConfig();
|
|
1778
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1779
|
+
await mkdir(codexDir, { recursive: true });
|
|
1780
|
+
await writeFile(configPath, config);
|
|
1781
|
+
await writeFile(hooksPath, hooks);
|
|
1782
|
+
await assert.rejects(uninstall({
|
|
1783
|
+
scope: 'project',
|
|
1784
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1785
|
+
transactionFailureInjector: (stage) => {
|
|
1786
|
+
if (stage === 'before-remove')
|
|
1787
|
+
throw new Error('pre-destructive removal failure');
|
|
1788
|
+
},
|
|
1789
|
+
}), (error) => {
|
|
1790
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1791
|
+
assert.match(message, /pre-destructive removal failure/);
|
|
1792
|
+
assert.doesNotMatch(message, /manual recovery|stale rollback/i);
|
|
1793
|
+
return true;
|
|
1794
|
+
});
|
|
1795
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1796
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1797
|
+
assert.equal(existsSync(stagedPath), false);
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
finally {
|
|
1801
|
+
await rm(wd, { recursive: true, force: true });
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
it('removes exact historical root trust state but preserves nonmatching nested state', async () => {
|
|
1805
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-legacy-hook-state-'));
|
|
1806
|
+
try {
|
|
1807
|
+
const home = join(wd, 'home');
|
|
1808
|
+
const codexDir = join(home, '.codex');
|
|
1809
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1810
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1811
|
+
await mkdir(codexDir, { recursive: true });
|
|
1812
|
+
await writeFile(configPath, buildOmxConfig());
|
|
1813
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
1814
|
+
state: {
|
|
1815
|
+
[`${hooksPath}:stop:0:0`]: { trusted_hash: 'sha256:historical' },
|
|
1816
|
+
},
|
|
1817
|
+
}, null, 2) + '\n');
|
|
1818
|
+
const rootStateCleanup = runOmx(wd, ['uninstall'], { HOME: home });
|
|
1819
|
+
if (shouldSkipForSpawnPermissions(rootStateCleanup.error))
|
|
1820
|
+
return;
|
|
1821
|
+
assert.equal(rootStateCleanup.status, 0, rootStateCleanup.stderr || rootStateCleanup.stdout);
|
|
1822
|
+
assert.deepEqual(JSON.parse(await readFile(hooksPath, 'utf-8')), {});
|
|
1823
|
+
const nestedState = {
|
|
1824
|
+
retained: { custom: true, trusted_hash: 'sha256:not-omx' },
|
|
1825
|
+
};
|
|
1826
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
1827
|
+
hooks: {
|
|
1828
|
+
state: nestedState,
|
|
1829
|
+
SessionStart: [{
|
|
1830
|
+
hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }],
|
|
1831
|
+
}],
|
|
381
1832
|
},
|
|
382
|
-
version: 1,
|
|
383
1833
|
}, null, 2) + '\n');
|
|
1834
|
+
const nestedStateCleanup = runOmx(wd, ['uninstall', '--keep-config'], { HOME: home });
|
|
1835
|
+
assert.equal(nestedStateCleanup.status, 0, nestedStateCleanup.stderr || nestedStateCleanup.stdout);
|
|
1836
|
+
const after = JSON.parse(await readFile(hooksPath, 'utf-8'));
|
|
1837
|
+
assert.deepEqual(after.hooks?.state, nestedState);
|
|
1838
|
+
assert.equal(after.hooks?.SessionStart, undefined);
|
|
1839
|
+
}
|
|
1840
|
+
finally {
|
|
1841
|
+
await rm(wd, { recursive: true, force: true });
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
it('removes only trust state at the managed hook coordinates planned from hooks.json', async () => {
|
|
1845
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-hook-trust-coordinates-'));
|
|
1846
|
+
try {
|
|
1847
|
+
const home = join(wd, 'home');
|
|
1848
|
+
const codexDir = join(home, '.codex');
|
|
1849
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1850
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1851
|
+
const hooks = JSON.stringify({
|
|
1852
|
+
hooks: {
|
|
1853
|
+
SessionStart: [{
|
|
1854
|
+
hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }],
|
|
1855
|
+
}],
|
|
1856
|
+
},
|
|
1857
|
+
}, null, 2) + '\n';
|
|
1858
|
+
const trustState = buildManagedCodexHookTrustState(hooksPath, wd, {
|
|
1859
|
+
hooksContent: hooks,
|
|
1860
|
+
});
|
|
1861
|
+
const [actualKey, actualTrust] = Object.entries(trustState)[0] ?? [];
|
|
1862
|
+
if (!actualKey || !actualTrust) {
|
|
1863
|
+
assert.fail('expected SessionStart managed trust state');
|
|
1864
|
+
}
|
|
1865
|
+
const staleCoordinate = `${hooksPath}:session_start:9:0`;
|
|
1866
|
+
const actualHeader = `[hooks.state.${JSON.stringify(actualKey)}]`;
|
|
1867
|
+
const staleHeader = `[hooks.state.${JSON.stringify(staleCoordinate)}]`;
|
|
1868
|
+
const config = [
|
|
1869
|
+
buildOmxConfig().trimEnd(),
|
|
1870
|
+
'',
|
|
1871
|
+
actualHeader,
|
|
1872
|
+
`trusted_hash = "${actualTrust.trusted_hash}"`,
|
|
1873
|
+
'',
|
|
1874
|
+
staleHeader,
|
|
1875
|
+
`trusted_hash = "${actualTrust.trusted_hash}"`,
|
|
1876
|
+
'',
|
|
1877
|
+
].join('\n');
|
|
1878
|
+
await mkdir(codexDir, { recursive: true });
|
|
1879
|
+
await writeFile(configPath, config);
|
|
1880
|
+
await writeFile(hooksPath, hooks);
|
|
384
1881
|
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
385
1882
|
if (shouldSkipForSpawnPermissions(res.error))
|
|
386
1883
|
return;
|
|
387
1884
|
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
assert.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
1885
|
+
const cleaned = await readFile(configPath, 'utf-8');
|
|
1886
|
+
assert.equal(cleaned.includes(actualHeader), false);
|
|
1887
|
+
assert.equal(cleaned.includes(staleHeader), true);
|
|
1888
|
+
}
|
|
1889
|
+
finally {
|
|
1890
|
+
await rm(wd, { recursive: true, force: true });
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
it('keeps empty hooks.json byte-identical during dry-run and no-op uninstall', async () => {
|
|
1894
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-empty-hooks-'));
|
|
1895
|
+
try {
|
|
1896
|
+
const home = join(wd, 'home');
|
|
1897
|
+
const codexDir = join(home, '.codex');
|
|
1898
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1899
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1900
|
+
const config = buildOmxConfig();
|
|
1901
|
+
const hooks = '{}\n';
|
|
1902
|
+
await mkdir(codexDir, { recursive: true });
|
|
1903
|
+
await writeFile(configPath, config);
|
|
1904
|
+
await writeFile(hooksPath, hooks);
|
|
1905
|
+
const dryRun = runOmx(wd, ['uninstall', '--dry-run'], { HOME: home });
|
|
1906
|
+
if (shouldSkipForSpawnPermissions(dryRun.error))
|
|
1907
|
+
return;
|
|
1908
|
+
assert.equal(dryRun.status, 0, dryRun.stderr || dryRun.stdout);
|
|
1909
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1910
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1911
|
+
const uninstall = runOmx(wd, ['uninstall'], { HOME: home });
|
|
1912
|
+
assert.equal(uninstall.status, 0, uninstall.stderr || uninstall.stdout);
|
|
1913
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1914
|
+
}
|
|
1915
|
+
finally {
|
|
1916
|
+
await rm(wd, { recursive: true, force: true });
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
it('fails closed for partial hooks.json corruption before config cleanup', async () => {
|
|
1920
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-corrupt-hooks-'));
|
|
1921
|
+
try {
|
|
1922
|
+
const home = join(wd, 'home');
|
|
1923
|
+
const codexDir = join(home, '.codex');
|
|
1924
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1925
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1926
|
+
const config = buildOmxConfig();
|
|
1927
|
+
const hooks = '{\n "hooks": { "SessionStart": {} }\n}\n';
|
|
1928
|
+
await mkdir(codexDir, { recursive: true });
|
|
1929
|
+
await writeFile(configPath, config);
|
|
1930
|
+
await writeFile(hooksPath, hooks);
|
|
1931
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
1932
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
1933
|
+
return;
|
|
1934
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
1935
|
+
assert.match(res.stderr, /SessionStart must be an array/);
|
|
1936
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1937
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
399
1938
|
}
|
|
400
1939
|
finally {
|
|
401
1940
|
await rm(wd, { recursive: true, force: true });
|
|
@@ -411,10 +1950,11 @@ describe('omx uninstall', () => {
|
|
|
411
1950
|
await writeFile(join(codexDir, 'hooks.json'), JSON.stringify({
|
|
412
1951
|
hooks: {
|
|
413
1952
|
SessionStart: [
|
|
1953
|
+
{ hooks: [{ type: 'command', command: 'echo keep-me' }] },
|
|
414
1954
|
{
|
|
1955
|
+
matcher: 'startup|resume|clear',
|
|
415
1956
|
hooks: [
|
|
416
1957
|
{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' },
|
|
417
|
-
{ type: 'command', command: 'echo keep-me' },
|
|
418
1958
|
],
|
|
419
1959
|
},
|
|
420
1960
|
],
|
|
@@ -450,6 +1990,7 @@ describe('omx uninstall', () => {
|
|
|
450
1990
|
assert.doesNotMatch(config, /^model_context_window = 250000$/m);
|
|
451
1991
|
assert.doesNotMatch(config, /^model_auto_compact_token_limit = 200000$/m);
|
|
452
1992
|
assert.doesNotMatch(config, /seeded behavioral defaults/);
|
|
1993
|
+
assert.match(config, /^multi_agent = true$/m);
|
|
453
1994
|
assert.doesNotMatch(config, /notify\s*=/);
|
|
454
1995
|
assert.doesNotMatch(config, /model_reasoning_effort\s*=/);
|
|
455
1996
|
assert.doesNotMatch(config, /developer_instructions\s*=/);
|
|
@@ -475,6 +2016,7 @@ describe('omx uninstall', () => {
|
|
|
475
2016
|
assert.match(config, /^model_context_window = 123456$/m);
|
|
476
2017
|
assert.match(config, /^model_auto_compact_token_limit = 200000$/m);
|
|
477
2018
|
assert.doesNotMatch(config, /seeded behavioral defaults/);
|
|
2019
|
+
assert.match(config, /^multi_agent = true$/m);
|
|
478
2020
|
assert.doesNotMatch(config, /notify\s*=/);
|
|
479
2021
|
assert.doesNotMatch(config, /model_reasoning_effort\s*=/);
|
|
480
2022
|
assert.doesNotMatch(config, /developer_instructions\s*=/);
|
|
@@ -527,6 +2069,56 @@ describe('omx uninstall', () => {
|
|
|
527
2069
|
await rm(wd, { recursive: true, force: true });
|
|
528
2070
|
}
|
|
529
2071
|
});
|
|
2072
|
+
for (const scope of ['user', 'project']) {
|
|
2073
|
+
for (const variant of [
|
|
2074
|
+
{ name: 'legacy-looking', multiAgent: true, maxThreads: 6, maxDepth: 2 },
|
|
2075
|
+
{ name: 'custom', multiAgent: false, maxThreads: 17, maxDepth: 5 },
|
|
2076
|
+
]) {
|
|
2077
|
+
it(`preserves ambiguous multi-agent ownership in ${scope} scope (${variant.name})`, async () => {
|
|
2078
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-ownership-'));
|
|
2079
|
+
try {
|
|
2080
|
+
const home = join(wd, 'home');
|
|
2081
|
+
const userConfigPath = join(home, '.codex', 'config.toml');
|
|
2082
|
+
const projectConfigPath = join(wd, '.codex', 'config.toml');
|
|
2083
|
+
const setupStateDir = join(wd, '.omx');
|
|
2084
|
+
await mkdir(join(home, '.codex'), { recursive: true });
|
|
2085
|
+
await mkdir(join(wd, '.codex'), { recursive: true });
|
|
2086
|
+
await mkdir(setupStateDir, { recursive: true });
|
|
2087
|
+
const userConfig = buildAmbiguousMultiAgentConfig('user-config', variant);
|
|
2088
|
+
const projectConfig = buildAmbiguousMultiAgentConfig('project-config', variant);
|
|
2089
|
+
await writeFile(userConfigPath, userConfig);
|
|
2090
|
+
await writeFile(projectConfigPath, projectConfig);
|
|
2091
|
+
await writeFile(join(setupStateDir, 'setup-scope.json'), `${JSON.stringify({ scope })}\n`);
|
|
2092
|
+
const selectedPath = scope === 'user' ? userConfigPath : projectConfigPath;
|
|
2093
|
+
const unselectedPath = scope === 'user' ? projectConfigPath : userConfigPath;
|
|
2094
|
+
const unselectedBefore = await readFile(unselectedPath, 'utf-8');
|
|
2095
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
2096
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
2097
|
+
return;
|
|
2098
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
2099
|
+
assert.match(res.stdout, new RegExp(`Resolved scope: ${scope}`));
|
|
2100
|
+
const selectedAfter = await readFile(selectedPath, 'utf-8');
|
|
2101
|
+
const parsed = TOML.parse(selectedAfter);
|
|
2102
|
+
assert.equal(parsed.sentinel, `${scope}-config`);
|
|
2103
|
+
assert.equal(parsed.features?.multi_agent, variant.multiAgent);
|
|
2104
|
+
assert.equal(parsed.features?.custom_feature, true);
|
|
2105
|
+
assert.equal(parsed.features?.child_agents_md, undefined);
|
|
2106
|
+
assert.equal(parsed.features?.goals, undefined);
|
|
2107
|
+
assert.equal(parsed.agents?.max_threads, variant.maxThreads);
|
|
2108
|
+
assert.equal(parsed.agents?.max_depth, variant.maxDepth);
|
|
2109
|
+
assert.deepEqual(parsed.agents?.custom_role, {
|
|
2110
|
+
description: `${scope}-config`,
|
|
2111
|
+
});
|
|
2112
|
+
assert.doesNotMatch(selectedAfter, /mcp_servers\.omx_state/);
|
|
2113
|
+
assert.doesNotMatch(selectedAfter, /oh-my-codex \(OMX\) Configuration/);
|
|
2114
|
+
assert.equal(await readFile(unselectedPath, 'utf-8'), unselectedBefore, 'unselected scope config must remain byte-identical');
|
|
2115
|
+
}
|
|
2116
|
+
finally {
|
|
2117
|
+
await rm(wd, { recursive: true, force: true });
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
530
2122
|
it('works with project scope', async () => {
|
|
531
2123
|
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-'));
|
|
532
2124
|
try {
|
|
@@ -768,6 +2360,25 @@ describe('omx uninstall', () => {
|
|
|
768
2360
|
await rm(wd, { recursive: true, force: true });
|
|
769
2361
|
}
|
|
770
2362
|
});
|
|
2363
|
+
it('removes OMX-managed AGENTS sections while preserving project guidance', async () => {
|
|
2364
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-merged-agents-'));
|
|
2365
|
+
try {
|
|
2366
|
+
const home = join(wd, 'home');
|
|
2367
|
+
await mkdir(home, { recursive: true });
|
|
2368
|
+
await mkdir(join(wd, '.omx'), { recursive: true });
|
|
2369
|
+
await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
|
|
2370
|
+
const userGuidance = '# User project instructions\n\nPreserve this guidance.\n';
|
|
2371
|
+
await writeFile(join(wd, 'AGENTS.md'), `${userGuidance}\n<!-- OMX:AGENTS:START -->\n<!-- omx:generated:agents-md -->\n# oh-my-codex - Intelligent Multi-Agent Orchestration\n<!-- OMX:AGENTS:END -->\n`);
|
|
2372
|
+
const res = runOmx(wd, ['uninstall', '--keep-config'], { HOME: home });
|
|
2373
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
2374
|
+
return;
|
|
2375
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
2376
|
+
assert.equal(await readFile(join(wd, 'AGENTS.md'), 'utf-8'), userGuidance);
|
|
2377
|
+
}
|
|
2378
|
+
finally {
|
|
2379
|
+
await rm(wd, { recursive: true, force: true });
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
771
2382
|
it('removes managed user-scope AGENTS.md from CODEX_HOME', async () => {
|
|
772
2383
|
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-'));
|
|
773
2384
|
try {
|
|
@@ -816,6 +2427,420 @@ describe('omx uninstall', () => {
|
|
|
816
2427
|
await rm(wd, { recursive: true, force: true });
|
|
817
2428
|
}
|
|
818
2429
|
});
|
|
2430
|
+
it('removes exact marked pairs and authorized singleton blocks during uninstall', async () => {
|
|
2431
|
+
const cases = [
|
|
2432
|
+
{ lines: ['# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults'], removed: ['model_context_window', 'model_auto_compact_token_limit'], preserved: {} },
|
|
2433
|
+
{ lines: ['model_auto_compact_token_limit=777', '# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', '# End oh-my-codex seeded behavioral defaults'], removed: ['model_context_window'], preserved: { model_auto_compact_token_limit: 777 } },
|
|
2434
|
+
{ lines: ['model_context_window = 123456', '# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults'], removed: ['model_auto_compact_token_limit'], preserved: { model_context_window: 123456 } },
|
|
2435
|
+
];
|
|
2436
|
+
for (const fixture of cases) {
|
|
2437
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-defaults-'));
|
|
2438
|
+
try {
|
|
2439
|
+
const home = join(wd, 'home');
|
|
2440
|
+
const configPath = join(home, '.codex', 'config.toml');
|
|
2441
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2442
|
+
await writeFile(configPath, `${fixture.lines.join('\n')}\n`);
|
|
2443
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
2444
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
2445
|
+
return;
|
|
2446
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
2447
|
+
const config = await readFile(configPath, 'utf-8');
|
|
2448
|
+
const parsed = TOML.parse(config);
|
|
2449
|
+
assert.doesNotMatch(config, /seeded behavioral defaults/);
|
|
2450
|
+
for (const key of fixture.removed)
|
|
2451
|
+
assert.equal(parsed[key], undefined);
|
|
2452
|
+
for (const [key, value] of Object.entries(fixture.preserved))
|
|
2453
|
+
assert.equal(parsed[key], value);
|
|
2454
|
+
}
|
|
2455
|
+
finally {
|
|
2456
|
+
await rm(wd, { recursive: true, force: true });
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
it('preserves unmarked and edited values, retaining only ambiguous ownership markers', async () => {
|
|
2461
|
+
const cases = [
|
|
2462
|
+
{
|
|
2463
|
+
lines: ['model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '[user_table]', 'label = "unmarked"'],
|
|
2464
|
+
preservedLines: ['model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '[user_table]', 'label = "unmarked"'],
|
|
2465
|
+
markers: false,
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
lines: ['# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 123456', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults', '[user_table]', 'label = "edited"'],
|
|
2469
|
+
preservedLines: ['model_context_window = 123456', 'model_auto_compact_token_limit = 200000', '[user_table]', 'label = "edited"'],
|
|
2470
|
+
markers: false,
|
|
2471
|
+
},
|
|
2472
|
+
{
|
|
2473
|
+
lines: ['model_auto_compact_token_limit = 1', 'model_auto_compact_token_limit = 2', '# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', '# End oh-my-codex seeded behavioral defaults', '[user_table]', 'label = "ambiguous"'],
|
|
2474
|
+
preservedLines: ['model_auto_compact_token_limit = 1', 'model_auto_compact_token_limit = 2', 'model_context_window = 250000', '[user_table]', 'label = "ambiguous"'],
|
|
2475
|
+
markers: true,
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
lines: ['model_context_window = 999', '# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults', '[user_table]', 'label = "pair-duplicate-before"'],
|
|
2479
|
+
preservedLines: ['model_context_window = 999', 'model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '[user_table]', 'label = "pair-duplicate-before"'],
|
|
2480
|
+
markers: true,
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
lines: ['# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults', 'model_auto_compact_token_limit = 999', '[user_table]', 'label = "pair-duplicate-after"'],
|
|
2484
|
+
preservedLines: ['model_context_window = 250000', 'model_auto_compact_token_limit = 200000', 'model_auto_compact_token_limit = 999', '[user_table]', 'label = "pair-duplicate-after"'],
|
|
2485
|
+
markers: true,
|
|
2486
|
+
},
|
|
2487
|
+
{
|
|
2488
|
+
lines: ['# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', '# End oh-my-codex seeded behavioral defaults', '[user_table]', 'label = "after-table"', 'model_auto_compact_token_limit = 777'],
|
|
2489
|
+
preservedLines: ['model_context_window = 250000', '[user_table]', 'label = "after-table"', 'model_auto_compact_token_limit = 777'],
|
|
2490
|
+
markers: false,
|
|
2491
|
+
},
|
|
2492
|
+
];
|
|
2493
|
+
for (const fixture of cases) {
|
|
2494
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-defaults-'));
|
|
2495
|
+
try {
|
|
2496
|
+
const home = join(wd, 'home');
|
|
2497
|
+
const configPath = join(home, '.codex', 'config.toml');
|
|
2498
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2499
|
+
await writeFile(configPath, `${fixture.lines.join('\n')}\n`);
|
|
2500
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
2501
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
2502
|
+
return;
|
|
2503
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
2504
|
+
const config = await readFile(configPath, 'utf-8');
|
|
2505
|
+
for (const line of fixture.preservedLines)
|
|
2506
|
+
assert.ok(config.includes(line), `missing preserved line: ${line}`);
|
|
2507
|
+
fixture.markers ? assert.match(config, /seeded behavioral defaults/) : assert.doesNotMatch(config, /seeded behavioral defaults/);
|
|
2508
|
+
}
|
|
2509
|
+
finally {
|
|
2510
|
+
await rm(wd, { recursive: true, force: true });
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
it('does not write during dry-run and is a fixed point after legacy-default cleanup', async () => {
|
|
2515
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-defaults-'));
|
|
2516
|
+
try {
|
|
2517
|
+
const home = join(wd, 'home');
|
|
2518
|
+
const configPath = join(home, '.codex', 'config.toml');
|
|
2519
|
+
const original = ['# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults', ''].join('\n');
|
|
2520
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2521
|
+
await writeFile(configPath, original);
|
|
2522
|
+
const dryRun = runOmx(wd, ['uninstall', '--dry-run'], { HOME: home });
|
|
2523
|
+
if (shouldSkipForSpawnPermissions(dryRun.error))
|
|
2524
|
+
return;
|
|
2525
|
+
assert.equal(dryRun.status, 0, dryRun.stderr || dryRun.stdout);
|
|
2526
|
+
assert.equal(await readFile(configPath, 'utf-8'), original);
|
|
2527
|
+
const first = runOmx(wd, ['uninstall'], { HOME: home });
|
|
2528
|
+
assert.equal(first.status, 0, first.stderr || first.stdout);
|
|
2529
|
+
const cleaned = await readFile(configPath, 'utf-8');
|
|
2530
|
+
const second = runOmx(wd, ['uninstall'], { HOME: home });
|
|
2531
|
+
assert.equal(second.status, 0, second.stderr || second.stdout);
|
|
2532
|
+
assert.equal(await readFile(configPath, 'utf-8'), cleaned);
|
|
2533
|
+
}
|
|
2534
|
+
finally {
|
|
2535
|
+
await rm(wd, { recursive: true, force: true });
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
it('keeps singleton and bounded cleanup dry-run-safe, differential, and idempotent', async () => {
|
|
2539
|
+
const start = '# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)';
|
|
2540
|
+
const end = '# End oh-my-codex seeded behavioral defaults';
|
|
2541
|
+
const fixtures = [
|
|
2542
|
+
{
|
|
2543
|
+
baseline: ['model_auto_compact_token_limit = 777', '[user_table]', 'label = "context-singleton"', ''].join('\n'),
|
|
2544
|
+
migrated: ['model_auto_compact_token_limit = 777', start, 'model_context_window = 250000', end, '[user_table]', 'label = "context-singleton"', ''].join('\n'),
|
|
2545
|
+
},
|
|
2546
|
+
{
|
|
2547
|
+
baseline: ['model_context_window = [', ' 123456,', ']', '[user_table]', 'label = "auto-singleton"', ''].join('\n'),
|
|
2548
|
+
migrated: ['model_context_window = [', ' 123456,', ']', start, 'model_auto_compact_token_limit = 200000', end, '[user_table]', 'label = "auto-singleton"', ''].join('\n'),
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
baseline: ['before = "keep"', 'model_context_window = 123456', '# interior comment', '[user_table]', 'label = "bounded"', ''].join('\n'),
|
|
2552
|
+
migrated: ['before = "keep"', start, 'model_context_window = 123456', '# interior comment', end, '[user_table]', 'label = "bounded"', ''].join('\n'),
|
|
2553
|
+
},
|
|
2554
|
+
];
|
|
2555
|
+
for (const fixture of fixtures) {
|
|
2556
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-defaults-'));
|
|
2557
|
+
try {
|
|
2558
|
+
const baselineHome = join(wd, 'baseline-home');
|
|
2559
|
+
const migratedHome = join(wd, 'migrated-home');
|
|
2560
|
+
const baselinePath = join(baselineHome, '.codex', 'config.toml');
|
|
2561
|
+
const migratedPath = join(migratedHome, '.codex', 'config.toml');
|
|
2562
|
+
await mkdir(dirname(baselinePath), { recursive: true });
|
|
2563
|
+
await mkdir(dirname(migratedPath), { recursive: true });
|
|
2564
|
+
await writeFile(baselinePath, fixture.baseline);
|
|
2565
|
+
await writeFile(migratedPath, fixture.migrated);
|
|
2566
|
+
const dryRun = runOmx(wd, ['uninstall', '--dry-run'], { HOME: migratedHome });
|
|
2567
|
+
if (shouldSkipForSpawnPermissions(dryRun.error))
|
|
2568
|
+
return;
|
|
2569
|
+
assert.equal(dryRun.status, 0, dryRun.stderr || dryRun.stdout);
|
|
2570
|
+
assert.equal(await readFile(migratedPath, 'utf-8'), fixture.migrated);
|
|
2571
|
+
const baselineRun = runOmx(wd, ['uninstall'], { HOME: baselineHome });
|
|
2572
|
+
assert.equal(baselineRun.status, 0, baselineRun.stderr || baselineRun.stdout);
|
|
2573
|
+
const migratedRun = runOmx(wd, ['uninstall'], { HOME: migratedHome });
|
|
2574
|
+
assert.equal(migratedRun.status, 0, migratedRun.stderr || migratedRun.stdout);
|
|
2575
|
+
const cleaned = await readFile(migratedPath, 'utf-8');
|
|
2576
|
+
assert.equal(cleaned, await readFile(baselinePath, 'utf-8'));
|
|
2577
|
+
const repeated = runOmx(wd, ['uninstall'], { HOME: migratedHome });
|
|
2578
|
+
assert.equal(repeated.status, 0, repeated.stderr || repeated.stdout);
|
|
2579
|
+
assert.equal(await readFile(migratedPath, 'utf-8'), cleaned);
|
|
2580
|
+
}
|
|
2581
|
+
finally {
|
|
2582
|
+
await rm(wd, { recursive: true, force: true });
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
it('preserves existing uninstall-pipeline semantics after removing a marked pair', async () => {
|
|
2587
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-defaults-'));
|
|
2588
|
+
try {
|
|
2589
|
+
const baselineHome = join(wd, 'baseline-home');
|
|
2590
|
+
const migratedHome = join(wd, 'migrated-home');
|
|
2591
|
+
const baselinePath = join(baselineHome, '.codex', 'config.toml');
|
|
2592
|
+
const migratedPath = join(migratedHome, '.codex', 'config.toml');
|
|
2593
|
+
const baseline = buildMixedConfig();
|
|
2594
|
+
await mkdir(dirname(baselinePath), { recursive: true });
|
|
2595
|
+
await mkdir(dirname(migratedPath), { recursive: true });
|
|
2596
|
+
await writeFile(baselinePath, baseline);
|
|
2597
|
+
await writeFile(migratedPath, baseline.replace('model = "o4-mini"', ['model = "o4-mini"', '# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)', 'model_context_window = 250000', 'model_auto_compact_token_limit = 200000', '# End oh-my-codex seeded behavioral defaults'].join('\n')));
|
|
2598
|
+
const baselineRun = runOmx(wd, ['uninstall'], { HOME: baselineHome });
|
|
2599
|
+
if (shouldSkipForSpawnPermissions(baselineRun.error))
|
|
2600
|
+
return;
|
|
2601
|
+
assert.equal(baselineRun.status, 0, baselineRun.stderr || baselineRun.stdout);
|
|
2602
|
+
const migratedRun = runOmx(wd, ['uninstall'], { HOME: migratedHome });
|
|
2603
|
+
assert.equal(migratedRun.status, 0, migratedRun.stderr || migratedRun.stdout);
|
|
2604
|
+
assert.deepEqual(TOML.parse(await readFile(migratedPath, 'utf-8')), TOML.parse(await readFile(baselinePath, 'utf-8')));
|
|
2605
|
+
}
|
|
2606
|
+
finally {
|
|
2607
|
+
await rm(wd, { recursive: true, force: true });
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
it('preserves a same-byte foreign replacement made immediately after an uninstall rename', async () => {
|
|
2611
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-immediate-post-rename-'));
|
|
2612
|
+
try {
|
|
2613
|
+
await withCwd(wd, async () => {
|
|
2614
|
+
const codexDir = join(wd, '.codex');
|
|
2615
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2616
|
+
const foreignPath = join(codexDir, '.foreign-config-same-byte');
|
|
2617
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2618
|
+
let replacement;
|
|
2619
|
+
let replacedInode;
|
|
2620
|
+
await mkdir(codexDir, { recursive: true });
|
|
2621
|
+
await writeFile(configPath, config);
|
|
2622
|
+
await assert.rejects(uninstall({
|
|
2623
|
+
scope: 'project',
|
|
2624
|
+
transactionFailureInjector: async (stage) => {
|
|
2625
|
+
if (stage !== 'after-rename')
|
|
2626
|
+
return;
|
|
2627
|
+
replacement = readFileSync(configPath);
|
|
2628
|
+
const ownedInode = lstatSync(configPath).ino;
|
|
2629
|
+
await writeFile(foreignPath, replacement);
|
|
2630
|
+
assert.notEqual(lstatSync(foreignPath).ino, ownedInode);
|
|
2631
|
+
rmSync(configPath);
|
|
2632
|
+
renameSync(foreignPath, configPath);
|
|
2633
|
+
replacedInode = lstatSync(configPath).ino;
|
|
2634
|
+
},
|
|
2635
|
+
}), /Uninstall artifact rollback failed.*recovery preflight/);
|
|
2636
|
+
assert.ok(replacement);
|
|
2637
|
+
assert.ok(replacedInode);
|
|
2638
|
+
assert.deepEqual(await readFile(configPath), replacement);
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
finally {
|
|
2642
|
+
await rm(wd, { recursive: true, force: true });
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
it('preflights every staged uninstall recovery copy before rollback', async () => {
|
|
2646
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-rollback-recovery-preflight-'));
|
|
2647
|
+
try {
|
|
2648
|
+
await withCwd(wd, async () => {
|
|
2649
|
+
const codexDir = join(wd, '.codex');
|
|
2650
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2651
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2652
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
2653
|
+
const stagedShimPath = join(dirname(shimPath), '.shim-preflight-recovery');
|
|
2654
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2655
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
2656
|
+
platform: 'win32',
|
|
2657
|
+
codexHomeDir: codexDir,
|
|
2658
|
+
}), null, 2)}\n`, 'utf-8');
|
|
2659
|
+
await mkdir(codexDir, { recursive: true });
|
|
2660
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
2661
|
+
await writeFile(configPath, config);
|
|
2662
|
+
await writeFile(hooksPath, hooks);
|
|
2663
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
2664
|
+
await assert.rejects(uninstall({
|
|
2665
|
+
scope: 'project',
|
|
2666
|
+
transactionPlatform: 'win32',
|
|
2667
|
+
transactionTemporaryPath: (path, purpose) => path === shimPath && purpose === 'delete'
|
|
2668
|
+
? stagedShimPath
|
|
2669
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-preflight`),
|
|
2670
|
+
transactionFailureInjector: async (stage) => {
|
|
2671
|
+
if (stage !== 'after-config-commit')
|
|
2672
|
+
return;
|
|
2673
|
+
await rm(stagedShimPath);
|
|
2674
|
+
throw new Error('injected missing staged recovery copy');
|
|
2675
|
+
},
|
|
2676
|
+
}), /Uninstall artifact rollback failed.*recovery preflight/);
|
|
2677
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2678
|
+
assert.equal(existsSync(shimPath), false);
|
|
2679
|
+
assert.notDeepEqual(await readFile(configPath), config);
|
|
2680
|
+
assert.equal(existsSync(stagedShimPath), false);
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
finally {
|
|
2684
|
+
await rm(wd, { recursive: true, force: true });
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
it('stops later uninstall rollback restores when the first restored artifact drifts', async () => {
|
|
2688
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-rollback-restored-drift-'));
|
|
2689
|
+
try {
|
|
2690
|
+
await withCwd(wd, async () => {
|
|
2691
|
+
const codexDir = join(wd, '.codex');
|
|
2692
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2693
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2694
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
2695
|
+
const foreignConfigPath = join(codexDir, '.foreign-restored-config');
|
|
2696
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2697
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
2698
|
+
platform: 'win32',
|
|
2699
|
+
codexHomeDir: codexDir,
|
|
2700
|
+
}), null, 2)}\n`, 'utf-8');
|
|
2701
|
+
await mkdir(codexDir, { recursive: true });
|
|
2702
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
2703
|
+
await writeFile(configPath, config);
|
|
2704
|
+
await writeFile(hooksPath, hooks);
|
|
2705
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
2706
|
+
let rollbackRenameCount = 0;
|
|
2707
|
+
let drifted = false;
|
|
2708
|
+
await assert.rejects(uninstall({
|
|
2709
|
+
scope: 'project',
|
|
2710
|
+
transactionPlatform: 'win32',
|
|
2711
|
+
transactionFailureInjector: async (stage) => {
|
|
2712
|
+
if (stage === 'after-config-commit') {
|
|
2713
|
+
throw new Error('injected rollback start failure');
|
|
2714
|
+
}
|
|
2715
|
+
if (stage !== 'before-rollback-rename')
|
|
2716
|
+
return;
|
|
2717
|
+
rollbackRenameCount += 1;
|
|
2718
|
+
if (rollbackRenameCount !== 2)
|
|
2719
|
+
return;
|
|
2720
|
+
const restoredInode = lstatSync(configPath).ino;
|
|
2721
|
+
await writeFile(foreignConfigPath, config);
|
|
2722
|
+
assert.notEqual(lstatSync(foreignConfigPath).ino, restoredInode);
|
|
2723
|
+
rmSync(configPath);
|
|
2724
|
+
renameSync(foreignConfigPath, configPath);
|
|
2725
|
+
drifted = lstatSync(configPath).ino !== restoredInode;
|
|
2726
|
+
},
|
|
2727
|
+
}), /Uninstall artifact rollback failed/);
|
|
2728
|
+
assert.equal(rollbackRenameCount, 2);
|
|
2729
|
+
assert.equal(drifted, true);
|
|
2730
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
2731
|
+
assert.equal(existsSync(shimPath), false);
|
|
2732
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
finally {
|
|
2736
|
+
await rm(wd, { recursive: true, force: true });
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
it('preserves a foreign inode injected after final uninstall rename validation', async () => {
|
|
2740
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-final-rename-claim-'));
|
|
2741
|
+
try {
|
|
2742
|
+
await withCwd(wd, async () => {
|
|
2743
|
+
const codexDir = join(wd, '.codex');
|
|
2744
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2745
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2746
|
+
const foreignConfig = Buffer.from('model = "foreign-final-rename"\n', 'utf-8');
|
|
2747
|
+
await mkdir(codexDir, { recursive: true });
|
|
2748
|
+
await writeFile(configPath, config);
|
|
2749
|
+
let foreignInode;
|
|
2750
|
+
await assert.rejects(uninstall({
|
|
2751
|
+
scope: 'project',
|
|
2752
|
+
transactionFailureInjector: (stage) => {
|
|
2753
|
+
if (stage !== 'after-final-rename-validation')
|
|
2754
|
+
return;
|
|
2755
|
+
const foreignPath = join(codexDir, '.foreign-final-rename-config');
|
|
2756
|
+
writeFileSync(foreignPath, foreignConfig);
|
|
2757
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
2758
|
+
rmSync(configPath);
|
|
2759
|
+
renameSync(foreignPath, configPath);
|
|
2760
|
+
},
|
|
2761
|
+
}), /(?:replacement claim.*not the planned artifact|planned artifact .* changed)/);
|
|
2762
|
+
assert.ok(foreignInode);
|
|
2763
|
+
assert.equal(lstatSync(configPath).ino, foreignInode);
|
|
2764
|
+
assert.deepEqual(await readFile(configPath), foreignConfig);
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
finally {
|
|
2768
|
+
await rm(wd, { recursive: true, force: true });
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
it('preserves a foreign inode injected after final uninstall removal validation', async () => {
|
|
2772
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-final-remove-claim-'));
|
|
2773
|
+
try {
|
|
2774
|
+
await withCwd(wd, async () => {
|
|
2775
|
+
const codexDir = join(wd, '.codex');
|
|
2776
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2777
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2778
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2779
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`, 'utf-8');
|
|
2780
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', 'utf-8');
|
|
2781
|
+
await mkdir(codexDir, { recursive: true });
|
|
2782
|
+
await writeFile(configPath, config);
|
|
2783
|
+
await writeFile(hooksPath, hooks);
|
|
2784
|
+
let foreignInode;
|
|
2785
|
+
await assert.rejects(uninstall({
|
|
2786
|
+
scope: 'project',
|
|
2787
|
+
transactionFailureInjector: (stage) => {
|
|
2788
|
+
if (stage !== 'after-final-remove-validation')
|
|
2789
|
+
return;
|
|
2790
|
+
const foreignPath = join(codexDir, '.foreign-final-remove-hooks');
|
|
2791
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
2792
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
2793
|
+
rmSync(hooksPath);
|
|
2794
|
+
renameSync(foreignPath, hooksPath);
|
|
2795
|
+
},
|
|
2796
|
+
}), /removal claim.*not the planned artifact/);
|
|
2797
|
+
assert.ok(foreignInode);
|
|
2798
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
2799
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
2800
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
finally {
|
|
2804
|
+
await rm(wd, { recursive: true, force: true });
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
it('preserves a foreign inode injected after final uninstall rollback validation', async () => {
|
|
2808
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-final-restore-claim-'));
|
|
2809
|
+
try {
|
|
2810
|
+
await withCwd(wd, async () => {
|
|
2811
|
+
const codexDir = join(wd, '.codex');
|
|
2812
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2813
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2814
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2815
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`, 'utf-8');
|
|
2816
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', 'utf-8');
|
|
2817
|
+
await mkdir(codexDir, { recursive: true });
|
|
2818
|
+
await writeFile(configPath, config);
|
|
2819
|
+
await writeFile(hooksPath, hooks);
|
|
2820
|
+
let foreignInode;
|
|
2821
|
+
await assert.rejects(uninstall({
|
|
2822
|
+
scope: 'project',
|
|
2823
|
+
transactionFailureInjector: (stage) => {
|
|
2824
|
+
if (stage === 'before-config-commit')
|
|
2825
|
+
throw new Error('injected rollback start');
|
|
2826
|
+
if (stage !== 'after-final-restore-validation')
|
|
2827
|
+
return;
|
|
2828
|
+
const foreignPath = join(codexDir, '.foreign-final-restore-hooks');
|
|
2829
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
2830
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
2831
|
+
renameSync(foreignPath, hooksPath);
|
|
2832
|
+
},
|
|
2833
|
+
}), /Uninstall artifact rollback failed/);
|
|
2834
|
+
assert.ok(foreignInode);
|
|
2835
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
2836
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
2837
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
finally {
|
|
2841
|
+
await rm(wd, { recursive: true, force: true });
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
819
2844
|
});
|
|
820
2845
|
describe('stripOmxFeatureFlags', () => {
|
|
821
2846
|
it('removes OMX feature flags and preserves user flags', async () => {
|
|
@@ -861,5 +2886,21 @@ describe('stripOmxFeatureFlags', () => {
|
|
|
861
2886
|
const result = stripOmxFeatureFlags(config);
|
|
862
2887
|
assert.equal(result, config);
|
|
863
2888
|
});
|
|
2889
|
+
it('preserves multi_agent when explicitly requested', async () => {
|
|
2890
|
+
const { stripOmxFeatureFlags } = await import('../../config/generator.js');
|
|
2891
|
+
const config = [
|
|
2892
|
+
'[features]',
|
|
2893
|
+
'multi_agent = false',
|
|
2894
|
+
'child_agents_md = true',
|
|
2895
|
+
'goals = true',
|
|
2896
|
+
'web_search = true',
|
|
2897
|
+
'',
|
|
2898
|
+
].join('\n');
|
|
2899
|
+
const result = stripOmxFeatureFlags(config, { preserveMultiAgent: true });
|
|
2900
|
+
assert.match(result, /^multi_agent = false$/m);
|
|
2901
|
+
assert.doesNotMatch(result, /^child_agents_md\s*=/m);
|
|
2902
|
+
assert.doesNotMatch(result, /^goals\s*=/m);
|
|
2903
|
+
assert.match(result, /^web_search = true$/m);
|
|
2904
|
+
});
|
|
864
2905
|
});
|
|
865
2906
|
//# sourceMappingURL=uninstall.test.js.map
|