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,14 +1,16 @@
|
|
|
1
1
|
import { after, before, describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile, stat, } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { parse as parseToml } from "@iarna/toml";
|
|
8
|
-
import { setup } from "../setup.js";
|
|
8
|
+
import { decideWindowsNativeHookShimReference, setNativeHookTransactionFailureInjectorForTest, setSetupLatePhaseFailureInjectorForTest, setNativeHookTransactionPlatformForTest, setNativeHookTransactionTemporaryPathForTest, setup, } from "../setup.js";
|
|
9
|
+
import { resolveSetupRefreshArgs } from "../update.js";
|
|
9
10
|
import { uninstall } from "../uninstall.js";
|
|
10
11
|
import { OMX_DEVELOPER_INSTRUCTIONS, OMX_PLUGIN_DEVELOPER_INSTRUCTIONS, } from "../../config/generator.js";
|
|
11
12
|
import { materializePackagedOmxPluginCache, resolvePackagedOmxMarketplace, } from "../plugin-marketplace.js";
|
|
13
|
+
import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, } from "../../config/codex-hooks.js";
|
|
12
14
|
const packageRoot = process.cwd();
|
|
13
15
|
let previousPathForFakeCodex;
|
|
14
16
|
let fakeCodexBinDir = null;
|
|
@@ -96,6 +98,32 @@ async function withIsolatedUserHome(wd, fn) {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
async function withDriveQualifiedWindowsCodexHome(wd, fn) {
|
|
102
|
+
const previousCwd = process.cwd();
|
|
103
|
+
const previousHome = process.env.HOME;
|
|
104
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
105
|
+
const codexHomeDir = "C:\\Users\\omx\\.codex";
|
|
106
|
+
process.chdir(wd);
|
|
107
|
+
process.env.HOME = wd;
|
|
108
|
+
process.env.CODEX_HOME = codexHomeDir;
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
111
|
+
return await fn(codexHomeDir);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
process.chdir(previousCwd);
|
|
115
|
+
if (typeof previousHome === "string")
|
|
116
|
+
process.env.HOME = previousHome;
|
|
117
|
+
else
|
|
118
|
+
delete process.env.HOME;
|
|
119
|
+
if (typeof previousCodexHome === "string") {
|
|
120
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
delete process.env.CODEX_HOME;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
99
127
|
describe("notify setup scope", () => {
|
|
100
128
|
it("does not write unsupported project-scope notify", async () => {
|
|
101
129
|
const wd = await mkdtemp(join(tmpdir(), "omx-project-no-notify-"));
|
|
@@ -697,7 +725,7 @@ describe("omx setup install mode behavior", () => {
|
|
|
697
725
|
installMode: "legacy",
|
|
698
726
|
});
|
|
699
727
|
assert.match(output, /Next steps:/);
|
|
700
|
-
assert.match(output, /Native agent
|
|
728
|
+
assert.match(output, /Native agent role TOML files written to \.codex\/agents\/; use explicit agent_type when spawning OMX roles/);
|
|
701
729
|
});
|
|
702
730
|
}
|
|
703
731
|
finally {
|
|
@@ -1357,6 +1385,12 @@ describe("omx setup install mode behavior", () => {
|
|
|
1357
1385
|
assert.match(config, /Registered Codex plugin marketplace surfaces supply OMX workflows and plugin-scoped companion resources/);
|
|
1358
1386
|
assert.match(config, /User-installed skills may still live under ~\/.codex\/skills/);
|
|
1359
1387
|
assert.match(config, /native agent roles are installed as setup-owned Codex agent TOML files in plugin mode so agent_type routing works/i);
|
|
1388
|
+
assert.match(config, /When the native surface exposes `agent_type` role routing, set `agent_type` to an installed role and never omit it for OMX work/i);
|
|
1389
|
+
assert.match(config, /role_routing_unavailable/i);
|
|
1390
|
+
assert.match(config, /do not fabricate `agent_type`/i);
|
|
1391
|
+
assert.match(config, /OMX adapted role-pass protocol/i);
|
|
1392
|
+
assert.match(config, /pre-validated role intent in the OMX subagent ledger/i);
|
|
1393
|
+
assert.match(config, /never fake the role via a prompt label/i);
|
|
1360
1394
|
assert.doesNotMatch(config, /Native subagents live in \.codex\/agents/);
|
|
1361
1395
|
assert.doesNotMatch(config, /Treat installed prompts as narrower execution surfaces/);
|
|
1362
1396
|
assert.match(config, /^plugin_hooks = true$/m);
|
|
@@ -1463,7 +1497,7 @@ describe("omx setup install mode behavior", () => {
|
|
|
1463
1497
|
await rm(wd, { recursive: true, force: true });
|
|
1464
1498
|
}
|
|
1465
1499
|
});
|
|
1466
|
-
it("
|
|
1500
|
+
it("migrates historical unwrapped developer_instructions after prompting", async () => {
|
|
1467
1501
|
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
1468
1502
|
try {
|
|
1469
1503
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
@@ -1480,10 +1514,12 @@ describe("omx setup install mode behavior", () => {
|
|
|
1480
1514
|
return "refresh";
|
|
1481
1515
|
},
|
|
1482
1516
|
});
|
|
1483
|
-
assert.equal(promptCount,
|
|
1517
|
+
assert.equal(promptCount, 1);
|
|
1484
1518
|
const config = await readFile(configPath, "utf-8");
|
|
1485
|
-
assert.match(config,
|
|
1486
|
-
assert.
|
|
1519
|
+
assert.match(config, /<omx version=\\"1\\">You have oh-my-codex installed through Codex plugin mode/);
|
|
1520
|
+
assert.match(config, /When the native surface exposes `agent_type` role routing, set `agent_type` to an installed role and never omit it for OMX work/i);
|
|
1521
|
+
assert.match(config, /role_routing_unavailable/i);
|
|
1522
|
+
assert.match(config, /OMX adapted role-pass protocol/i);
|
|
1487
1523
|
assert.equal((config.match(/^developer_instructions\s*=/gm) ?? []).length, 1);
|
|
1488
1524
|
});
|
|
1489
1525
|
});
|
|
@@ -1734,20 +1770,773 @@ describe("omx setup install mode behavior", () => {
|
|
|
1734
1770
|
await rm(wd, { recursive: true, force: true });
|
|
1735
1771
|
}
|
|
1736
1772
|
});
|
|
1737
|
-
it("
|
|
1773
|
+
it("fails before plugin transition writes when marker-wrapped trust state has foreign siblings", async () => {
|
|
1774
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1775
|
+
const representations = [
|
|
1776
|
+
{
|
|
1777
|
+
name: "inline",
|
|
1778
|
+
render: (key, hash) => [
|
|
1779
|
+
`hooks = { state = { ${JSON.stringify(key)} = { trusted_hash = ${JSON.stringify(hash)}, foreign = true } } }`,
|
|
1780
|
+
],
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
name: "dotted",
|
|
1784
|
+
render: (key, hash) => [
|
|
1785
|
+
`hooks.state.${JSON.stringify(key)}.trusted_hash = ${JSON.stringify(hash)}`,
|
|
1786
|
+
`hooks.state.${JSON.stringify(key)}.foreign = true`,
|
|
1787
|
+
],
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
name: "table",
|
|
1791
|
+
render: (key, hash) => [
|
|
1792
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
1793
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
1794
|
+
"foreign = true",
|
|
1795
|
+
],
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
name: "separate-foreign-table",
|
|
1799
|
+
render: (key, hash) => [
|
|
1800
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
1801
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
1802
|
+
"",
|
|
1803
|
+
'[hooks.state."foreign-key"]',
|
|
1804
|
+
'trusted_hash = "sha256:foreign"',
|
|
1805
|
+
],
|
|
1806
|
+
},
|
|
1807
|
+
];
|
|
1808
|
+
try {
|
|
1809
|
+
for (const representation of representations) {
|
|
1810
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-plugin-trust-${representation.name}-`));
|
|
1811
|
+
try {
|
|
1812
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1813
|
+
await withTempCwd(wd, async () => {
|
|
1814
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
1815
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1816
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
1817
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
1818
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot, {
|
|
1819
|
+
platform: "win32",
|
|
1820
|
+
codexHomeDir,
|
|
1821
|
+
}), null, 2)}\n`;
|
|
1822
|
+
const [key, trust] = Object.entries(buildManagedCodexHookTrustState(hooksPath, packageRoot, {
|
|
1823
|
+
platform: "win32",
|
|
1824
|
+
codexHomeDir,
|
|
1825
|
+
hooksContent: hooks,
|
|
1826
|
+
}))[0] ?? [];
|
|
1827
|
+
assert.ok(key);
|
|
1828
|
+
assert.ok(trust);
|
|
1829
|
+
const config = [
|
|
1830
|
+
'model = "foreign"',
|
|
1831
|
+
"",
|
|
1832
|
+
"# ============================================================",
|
|
1833
|
+
"# oh-my-codex (OMX) Configuration",
|
|
1834
|
+
"# Managed by omx setup - manual edits preserved on next setup",
|
|
1835
|
+
"# ============================================================",
|
|
1836
|
+
"",
|
|
1837
|
+
...representation.render(key, trust.trusted_hash),
|
|
1838
|
+
"",
|
|
1839
|
+
"# ============================================================",
|
|
1840
|
+
"# End oh-my-codex",
|
|
1841
|
+
"",
|
|
1842
|
+
].join("\n");
|
|
1843
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
1844
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot), "utf-8");
|
|
1845
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1846
|
+
await writeFile(configPath, config);
|
|
1847
|
+
await writeFile(hooksPath, hooks);
|
|
1848
|
+
await writeFile(shimPath, shim);
|
|
1849
|
+
await writeFile(metadataPath, metadata);
|
|
1850
|
+
await assert.rejects(setup({
|
|
1851
|
+
scope: "user",
|
|
1852
|
+
installMode: "plugin",
|
|
1853
|
+
pluginAgentsMdPrompt: async () => false,
|
|
1854
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1855
|
+
}), (error) => typeof error === "object" &&
|
|
1856
|
+
error !== null &&
|
|
1857
|
+
"code" in error &&
|
|
1858
|
+
error.code === "managed_trust_key_conflict");
|
|
1859
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, "utf-8"));
|
|
1860
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, "utf-8"));
|
|
1861
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
1862
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
1863
|
+
assert.deepEqual((await readdir(codexHomeDir)).filter((entry) => entry.includes(".omx-")), []);
|
|
1864
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes(".omx-")), []);
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
finally {
|
|
1869
|
+
await rm(wd, { recursive: true, force: true });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
finally {
|
|
1874
|
+
resetPlatform();
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
it("fails before plugin transition writes when marker trust has absent or non-managed hooks expectations", async () => {
|
|
1878
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
1879
|
+
const fixtures = [
|
|
1880
|
+
{ name: "absent", hooks: null },
|
|
1881
|
+
{
|
|
1882
|
+
name: "non-managed",
|
|
1883
|
+
hooks: `${JSON.stringify({
|
|
1884
|
+
hooks: {
|
|
1885
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: "echo foreign" }] }],
|
|
1886
|
+
},
|
|
1887
|
+
}, null, 2)}\n`,
|
|
1888
|
+
},
|
|
1889
|
+
];
|
|
1890
|
+
try {
|
|
1891
|
+
for (const fixture of fixtures) {
|
|
1892
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-plugin-marker-${fixture.name}-`));
|
|
1893
|
+
try {
|
|
1894
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1895
|
+
await withTempCwd(wd, async () => {
|
|
1896
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
1897
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1898
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
1899
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
1900
|
+
const config = [
|
|
1901
|
+
'model = "foreign"',
|
|
1902
|
+
"",
|
|
1903
|
+
"# ============================================================",
|
|
1904
|
+
"# oh-my-codex (OMX) Configuration",
|
|
1905
|
+
"# Managed by omx setup - manual edits preserved on next setup",
|
|
1906
|
+
"# ============================================================",
|
|
1907
|
+
"",
|
|
1908
|
+
'[hooks.state."foreign-key"]',
|
|
1909
|
+
'trusted_hash = "sha256:foreign"',
|
|
1910
|
+
"",
|
|
1911
|
+
"# ============================================================",
|
|
1912
|
+
"# End oh-my-codex",
|
|
1913
|
+
"",
|
|
1914
|
+
].join("\n");
|
|
1915
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot), "utf-8");
|
|
1916
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
1917
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1918
|
+
await writeFile(configPath, config);
|
|
1919
|
+
if (fixture.hooks !== null)
|
|
1920
|
+
await writeFile(hooksPath, fixture.hooks);
|
|
1921
|
+
await writeFile(shimPath, shim);
|
|
1922
|
+
await writeFile(metadataPath, metadata);
|
|
1923
|
+
await assert.rejects(setup({
|
|
1924
|
+
scope: "user",
|
|
1925
|
+
installMode: "plugin",
|
|
1926
|
+
pluginAgentsMdPrompt: async () => false,
|
|
1927
|
+
skipNativeAgentRefresh: true,
|
|
1928
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
1929
|
+
}), (error) => typeof error === "object" &&
|
|
1930
|
+
error !== null &&
|
|
1931
|
+
"code" in error &&
|
|
1932
|
+
error.code === "managed_trust_key_conflict");
|
|
1933
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, "utf-8"));
|
|
1934
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
1935
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
1936
|
+
if (fixture.hooks === null) {
|
|
1937
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(fixture.hooks, "utf-8"));
|
|
1941
|
+
}
|
|
1942
|
+
assert.deepEqual((await readdir(codexHomeDir)).filter((entry) => entry.includes(".omx-")), []);
|
|
1943
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes(".omx-")), []);
|
|
1944
|
+
});
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
finally {
|
|
1948
|
+
await rm(wd, { recursive: true, force: true });
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
finally {
|
|
1953
|
+
resetPlatform();
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
it("treats normalized managed Windows shim identity as exact and every distinct target as ambiguous", () => {
|
|
1957
|
+
const shimBasename = "omx-native-hook-windows-shim.ps1";
|
|
1958
|
+
const targetShimPath = `C:\\Users\\alice\\.codex\\hooks\\${shimBasename}`;
|
|
1959
|
+
const futureCommand = (shimPath) => `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`;
|
|
1960
|
+
const futureHooks = (shimPath) => JSON.stringify({
|
|
1961
|
+
hooks: {
|
|
1962
|
+
FutureEvent: [{ hooks: [{ type: "command", command: futureCommand(shimPath) }] }],
|
|
1963
|
+
},
|
|
1964
|
+
});
|
|
1965
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(`c:/users/alice/.codex/hooks/../hooks/${shimBasename.toUpperCase()}`), targetShimPath), "referenced");
|
|
1966
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(`D:\\Users\\alice\\.codex\\hooks\\${shimBasename}`), targetShimPath), "ambiguous");
|
|
1967
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks("D:\\Users\\alice\\.codex\\hooks\\unrelated.ps1"), targetShimPath), "ambiguous");
|
|
1968
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1969
|
+
hooks: {
|
|
1970
|
+
FutureEvent: [{ hooks: [{ type: "command", command: "& $env:OMX_SHIM" }] }],
|
|
1971
|
+
},
|
|
1972
|
+
}), targetShimPath), "ambiguous");
|
|
1973
|
+
for (const command of [
|
|
1974
|
+
"& %OMX_SHIM%",
|
|
1975
|
+
"& !OMX_SHIM!",
|
|
1976
|
+
"& $(Get-Item env:OMX_SHIM)",
|
|
1977
|
+
]) {
|
|
1978
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1979
|
+
hooks: {
|
|
1980
|
+
FutureEvent: [{ hooks: [{ type: "command", command }] }],
|
|
1981
|
+
},
|
|
1982
|
+
}), targetShimPath), "ambiguous", command);
|
|
1983
|
+
}
|
|
1984
|
+
for (const command of [
|
|
1985
|
+
`Write-Output "don't"; & $env:HOOK_SCRIPT; Write-Output "user's"`,
|
|
1986
|
+
"Invoke-Expression '& $env:HOOK_SCRIPT'",
|
|
1987
|
+
"cmd /c 'call %HOOK_SCRIPT%'",
|
|
1988
|
+
`& ('C:\\Users\\alice\\.codex\\hooks\\omx-native-hook-windows-' + 'shim.ps1')`,
|
|
1989
|
+
"& (Get-Content C:\\shim-path.txt)",
|
|
1990
|
+
]) {
|
|
1991
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1992
|
+
hooks: {
|
|
1993
|
+
FutureEvent: [{ hooks: [{ type: "command", command }] }],
|
|
1994
|
+
},
|
|
1995
|
+
}), targetShimPath), "ambiguous", command);
|
|
1996
|
+
}
|
|
1997
|
+
assert.equal(decideWindowsNativeHookShimReference(JSON.stringify({
|
|
1998
|
+
hooks: {
|
|
1999
|
+
FutureEvent: [{
|
|
2000
|
+
hooks: [{
|
|
2001
|
+
type: "command",
|
|
2002
|
+
command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
|
|
2003
|
+
}],
|
|
2004
|
+
}],
|
|
2005
|
+
},
|
|
2006
|
+
}), targetShimPath), "ambiguous");
|
|
2007
|
+
for (const ambiguousPath of [
|
|
2008
|
+
`\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2009
|
+
`C:Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2010
|
+
`C:\\\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2011
|
+
String.raw `\Users\ALICE~1\.codex\hooks\OMX-NA~1.PS1`,
|
|
2012
|
+
String.raw `C:Users\ALICE~1\.codex\hooks\OMX-NA~1.PS1`,
|
|
2013
|
+
`\\\\?\\C:\\Users\\alice\\.codex\\hooks\\${shimBasename}`,
|
|
2014
|
+
`C:\\$env:USERPROFILE\\hooks\\${shimBasename}`,
|
|
2015
|
+
`.\\hooks\\${shimBasename}`,
|
|
2016
|
+
`D:\\aliases\\${shimBasename}. `,
|
|
2017
|
+
`C:\\Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1`,
|
|
2018
|
+
]) {
|
|
2019
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(ambiguousPath), targetShimPath), "ambiguous", ambiguousPath);
|
|
2020
|
+
}
|
|
2021
|
+
const uncTargetShimPath = `\\\\server\\share\\users\\alice\\.codex\\hooks\\${shimBasename}`;
|
|
2022
|
+
assert.equal(decideWindowsNativeHookShimReference(futureHooks(`//SERVER/share/users/alice/.codex/hooks/../hooks/${shimBasename.toUpperCase()}`), uncTargetShimPath), "referenced");
|
|
2023
|
+
});
|
|
2024
|
+
it("rolls back when an environment-indirected preserved Windows shim drifts during hooks or config writes", async () => {
|
|
2025
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2026
|
+
try {
|
|
2027
|
+
for (const mutationKind of ["hooks", "config"]) {
|
|
2028
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-plugin-preserved-shim-${mutationKind}-`));
|
|
2029
|
+
let resetFailureInjector;
|
|
2030
|
+
try {
|
|
2031
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2032
|
+
await withTempCwd(wd, async () => {
|
|
2033
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2034
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2035
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2036
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2037
|
+
platform: "win32",
|
|
2038
|
+
codexHomeDir,
|
|
2039
|
+
});
|
|
2040
|
+
const hooksBefore = Buffer.from(`${JSON.stringify({
|
|
2041
|
+
...managed,
|
|
2042
|
+
hooks: {
|
|
2043
|
+
...managed.hooks,
|
|
2044
|
+
FutureEvent: [{
|
|
2045
|
+
hooks: [{ type: "command", command: "& $env:OMX_SHIM" }],
|
|
2046
|
+
}],
|
|
2047
|
+
},
|
|
2048
|
+
}, null, 2)}\n`, "utf-8");
|
|
2049
|
+
const foreignShim = Buffer.from(`# concurrent ${mutationKind} shim replacement\n`, "utf-8");
|
|
2050
|
+
await writeFile(hooksPath, hooksBefore);
|
|
2051
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2052
|
+
let injected = false;
|
|
2053
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
2054
|
+
if (stage !== "before_temp_write" ||
|
|
2055
|
+
target.kind !== mutationKind ||
|
|
2056
|
+
injected) {
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
injected = true;
|
|
2060
|
+
writeFileSync(shimPath, foreignShim);
|
|
2061
|
+
});
|
|
2062
|
+
await assert.rejects(setup({
|
|
2063
|
+
scope: "user",
|
|
2064
|
+
installMode: "plugin",
|
|
2065
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2066
|
+
skipNativeAgentRefresh: true,
|
|
2067
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2068
|
+
}), /precondition changed/);
|
|
2069
|
+
assert.equal(injected, true, mutationKind);
|
|
2070
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
2071
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2072
|
+
assert.equal(existsSync(configPath), false);
|
|
2073
|
+
});
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
finally {
|
|
2077
|
+
resetFailureInjector?.();
|
|
2078
|
+
await rm(wd, { recursive: true, force: true });
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
finally {
|
|
2083
|
+
resetPlatform();
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
it("rolls back when a preserved Windows shim drifts after staged cleanup finalization", async () => {
|
|
2087
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-preserved-shim-finalization-"));
|
|
2088
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2089
|
+
let resetFailureInjector;
|
|
2090
|
+
try {
|
|
2091
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2092
|
+
await withTempCwd(wd, async () => {
|
|
2093
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2094
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2095
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2096
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2097
|
+
platform: "win32",
|
|
2098
|
+
codexHomeDir,
|
|
2099
|
+
});
|
|
2100
|
+
const hooksBefore = Buffer.from(`${JSON.stringify({
|
|
2101
|
+
...managed,
|
|
2102
|
+
hooks: {
|
|
2103
|
+
...managed.hooks,
|
|
2104
|
+
FutureEvent: [{
|
|
2105
|
+
hooks: [{ type: "command", command: "& $env:OMX_SHIM" }],
|
|
2106
|
+
}],
|
|
2107
|
+
},
|
|
2108
|
+
}, null, 2)}\n`, "utf-8");
|
|
2109
|
+
const foreignShim = Buffer.from("# concurrent finalization shim replacement\n", "utf-8");
|
|
2110
|
+
await writeFile(hooksPath, hooksBefore);
|
|
2111
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2112
|
+
let injected = false;
|
|
2113
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
2114
|
+
if (stage !== "after_staged_cleanup" || injected)
|
|
2115
|
+
return;
|
|
2116
|
+
injected = true;
|
|
2117
|
+
writeFileSync(shimPath, foreignShim);
|
|
2118
|
+
});
|
|
2119
|
+
await assert.rejects(setup({
|
|
2120
|
+
scope: "user",
|
|
2121
|
+
installMode: "plugin",
|
|
2122
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2123
|
+
skipNativeAgentRefresh: true,
|
|
2124
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2125
|
+
}), /precondition changed/);
|
|
2126
|
+
assert.equal(injected, true);
|
|
2127
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
2128
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2129
|
+
assert.equal(existsSync(configPath), false);
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
finally {
|
|
2134
|
+
resetFailureInjector?.();
|
|
2135
|
+
resetPlatform();
|
|
2136
|
+
await rm(wd, { recursive: true, force: true });
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
it("preserves a drive-qualified shim for a drive-less future reference and aborts stale plugin transitions", async () => {
|
|
2140
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-drive-qualified-future-shim-"));
|
|
2141
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2142
|
+
let resetFailureInjector;
|
|
2143
|
+
try {
|
|
2144
|
+
await withDriveQualifiedWindowsCodexHome(wd, async (codexHomeDir) => {
|
|
2145
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2146
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2147
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2148
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2149
|
+
platform: "win32",
|
|
2150
|
+
codexHomeDir,
|
|
2151
|
+
});
|
|
2152
|
+
const driveLessFutureShimPath = "\\Users\\omx\\.codex\\hooks\\omx-native-hook-windows-shim.ps1";
|
|
2153
|
+
const futureHooks = `${JSON.stringify({
|
|
2154
|
+
...managed,
|
|
2155
|
+
hooks: {
|
|
2156
|
+
...managed.hooks,
|
|
2157
|
+
FutureEvent: [{
|
|
2158
|
+
hooks: [{
|
|
2159
|
+
type: "command",
|
|
2160
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${driveLessFutureShimPath}'`,
|
|
2161
|
+
}],
|
|
2162
|
+
}],
|
|
2163
|
+
},
|
|
2164
|
+
}, null, 2)}\n`;
|
|
2165
|
+
const pluginSetup = () => setup({
|
|
2166
|
+
scope: "user",
|
|
2167
|
+
installMode: "plugin",
|
|
2168
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2169
|
+
skipNativeAgentRefresh: true,
|
|
2170
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2171
|
+
});
|
|
2172
|
+
await writeFile(hooksPath, futureHooks);
|
|
2173
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2174
|
+
await pluginSetup();
|
|
2175
|
+
assert.equal(existsSync(shimPath), true);
|
|
2176
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
2177
|
+
await writeFile(hooksPath, futureHooks);
|
|
2178
|
+
const hooksBefore = await readFile(hooksPath);
|
|
2179
|
+
const configBefore = await readFile(configPath);
|
|
2180
|
+
const foreignShim = Buffer.from("# concurrent foreign shim\n", "utf-8");
|
|
2181
|
+
let injected = false;
|
|
2182
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
2183
|
+
if (stage !== "before_precondition" || target.kind !== "shim" || injected) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
injected = true;
|
|
2187
|
+
writeFileSync(shimPath, foreignShim);
|
|
2188
|
+
});
|
|
2189
|
+
await assert.rejects(pluginSetup(), /precondition changed/);
|
|
2190
|
+
assert.equal(injected, true);
|
|
2191
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
2192
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2193
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
finally {
|
|
2197
|
+
resetFailureInjector?.();
|
|
2198
|
+
resetPlatform();
|
|
2199
|
+
await rm(wd, { recursive: true, force: true });
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
it("preserves a shim referenced by a Unicode-escaped future hook event", async () => {
|
|
2203
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-future-shim-"));
|
|
2204
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2205
|
+
try {
|
|
2206
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2207
|
+
await withTempCwd(wd, async () => {
|
|
2208
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2209
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2210
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2211
|
+
platform: "win32",
|
|
2212
|
+
codexHomeDir,
|
|
2213
|
+
});
|
|
2214
|
+
const sentinel = "__OMX_ESCAPED_SHIM_COMMAND__";
|
|
2215
|
+
const futureHooks = JSON.stringify({
|
|
2216
|
+
...managed,
|
|
2217
|
+
hooks: {
|
|
2218
|
+
...managed.hooks,
|
|
2219
|
+
FutureEvent: [{ hooks: [{ type: "command", command: sentinel }] }],
|
|
2220
|
+
},
|
|
2221
|
+
}, 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";
|
|
2222
|
+
await writeFile(hooksPath, futureHooks);
|
|
2223
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2224
|
+
await setup({
|
|
2225
|
+
scope: "user",
|
|
2226
|
+
installMode: "plugin",
|
|
2227
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2228
|
+
skipNativeAgentRefresh: true,
|
|
2229
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2230
|
+
});
|
|
2231
|
+
assert.equal(existsSync(shimPath), true);
|
|
2232
|
+
const finalHooks = await readFile(hooksPath, "utf-8");
|
|
2233
|
+
assert.match(finalHooks, /FutureEvent/);
|
|
2234
|
+
assert.match(finalHooks, /\\u005c/);
|
|
2235
|
+
assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
|
|
2236
|
+
});
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
finally {
|
|
2240
|
+
resetPlatform();
|
|
2241
|
+
await rm(wd, { recursive: true, force: true });
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
it("preserves a shim referenced through a normalized Windows path alias", async () => {
|
|
2245
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-shim-path-alias-"));
|
|
2246
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2247
|
+
try {
|
|
2248
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2249
|
+
await withTempCwd(wd, async () => {
|
|
2250
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2251
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2252
|
+
const shimAlias = shimPath
|
|
2253
|
+
.replace(/[\\/]+hooks[\\/]+/i, "\\hooks\\\\.\\")
|
|
2254
|
+
.replace(/omx-native-hook-windows-shim\.ps1$/i, "OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1")
|
|
2255
|
+
.replace(/\\/g, "/");
|
|
2256
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2257
|
+
platform: "win32",
|
|
2258
|
+
codexHomeDir,
|
|
2259
|
+
});
|
|
2260
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2261
|
+
...managed,
|
|
2262
|
+
hooks: {
|
|
2263
|
+
...managed.hooks,
|
|
2264
|
+
FutureEvent: [{
|
|
2265
|
+
hooks: [{
|
|
2266
|
+
type: "command",
|
|
2267
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`,
|
|
2268
|
+
}],
|
|
2269
|
+
}],
|
|
2270
|
+
},
|
|
2271
|
+
}, null, 2) + "\n");
|
|
2272
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2273
|
+
await setup({
|
|
2274
|
+
scope: "user",
|
|
2275
|
+
installMode: "plugin",
|
|
2276
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2277
|
+
skipNativeAgentRefresh: true,
|
|
2278
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2279
|
+
});
|
|
2280
|
+
assert.equal(existsSync(shimPath), true);
|
|
2281
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
finally {
|
|
2286
|
+
resetPlatform();
|
|
2287
|
+
await rm(wd, { recursive: true, force: true });
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
it("preserves a shim for a distinct absolute -File target", async () => {
|
|
2291
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-quoted-inert-shim-"));
|
|
2292
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2293
|
+
try {
|
|
2294
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2295
|
+
await withTempCwd(wd, async () => {
|
|
2296
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2297
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2298
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2299
|
+
platform: "win32",
|
|
2300
|
+
codexHomeDir,
|
|
2301
|
+
});
|
|
2302
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2303
|
+
...managed,
|
|
2304
|
+
hooks: {
|
|
2305
|
+
...managed.hooks,
|
|
2306
|
+
FutureEvent: [{
|
|
2307
|
+
hooks: [{
|
|
2308
|
+
type: "command",
|
|
2309
|
+
command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
|
|
2310
|
+
}],
|
|
2311
|
+
}],
|
|
2312
|
+
},
|
|
2313
|
+
}, null, 2) + "\n");
|
|
2314
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2315
|
+
await setup({
|
|
2316
|
+
scope: "user",
|
|
2317
|
+
installMode: "plugin",
|
|
2318
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2319
|
+
skipNativeAgentRefresh: true,
|
|
2320
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2321
|
+
});
|
|
2322
|
+
assert.equal(existsSync(shimPath), true);
|
|
2323
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
2324
|
+
});
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
finally {
|
|
2328
|
+
resetPlatform();
|
|
2329
|
+
await rm(wd, { recursive: true, force: true });
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
it("does not retain a shim for inert future-event metadata", async () => {
|
|
2333
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-plugin-future-shim-metadata-"));
|
|
2334
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2335
|
+
try {
|
|
2336
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2337
|
+
await withTempCwd(wd, async () => {
|
|
2338
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2339
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2340
|
+
const managed = buildManagedCodexHooksConfig(packageRoot, {
|
|
2341
|
+
platform: "win32",
|
|
2342
|
+
codexHomeDir,
|
|
2343
|
+
});
|
|
2344
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
2345
|
+
...managed,
|
|
2346
|
+
hooks: {
|
|
2347
|
+
...managed.hooks,
|
|
2348
|
+
FutureEvent: [
|
|
2349
|
+
{
|
|
2350
|
+
metadata: shimPath,
|
|
2351
|
+
hooks: [{ type: "agent", prompt: shimPath }],
|
|
2352
|
+
},
|
|
2353
|
+
],
|
|
2354
|
+
},
|
|
2355
|
+
}, null, 2) + "\n");
|
|
2356
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot));
|
|
2357
|
+
await setup({
|
|
2358
|
+
scope: "user",
|
|
2359
|
+
installMode: "plugin",
|
|
2360
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2361
|
+
skipNativeAgentRefresh: true,
|
|
2362
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
2363
|
+
});
|
|
2364
|
+
assert.equal(existsSync(shimPath), false);
|
|
2365
|
+
assert.match(await readFile(hooksPath, "utf-8"), /FutureEvent/);
|
|
2366
|
+
});
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
finally {
|
|
2370
|
+
resetPlatform();
|
|
2371
|
+
await rm(wd, { recursive: true, force: true });
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
it("orders Windows plugin-transition mutations as hooks, shim, then config", async () => {
|
|
2375
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-removal-"));
|
|
2376
|
+
const forwardOrder = [];
|
|
2377
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2378
|
+
let resetFailureInjector;
|
|
2379
|
+
try {
|
|
2380
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2381
|
+
await withTempCwd(wd, async () => {
|
|
2382
|
+
await setup({
|
|
2383
|
+
scope: "user",
|
|
2384
|
+
installMode: "legacy",
|
|
2385
|
+
skipNativeAgentRefresh: true,
|
|
2386
|
+
});
|
|
2387
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2388
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2389
|
+
assert.equal(existsSync(hooksPath), true);
|
|
2390
|
+
assert.equal(existsSync(shimPath), true);
|
|
2391
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
2392
|
+
if (stage === "before_temp_write")
|
|
2393
|
+
forwardOrder.push(artifact.kind);
|
|
2394
|
+
});
|
|
2395
|
+
await setup({
|
|
2396
|
+
scope: "user",
|
|
2397
|
+
installMode: "plugin",
|
|
2398
|
+
skipNativeAgentRefresh: true,
|
|
2399
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2400
|
+
codexFeaturesProbe: () => [
|
|
2401
|
+
"hooks stable true",
|
|
2402
|
+
"plugin_hooks experimental true",
|
|
2403
|
+
"",
|
|
2404
|
+
].join("\n"),
|
|
2405
|
+
});
|
|
2406
|
+
assert.deepEqual(forwardOrder, ["hooks", "shim", "config"]);
|
|
2407
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2408
|
+
assert.equal(existsSync(shimPath), false);
|
|
2409
|
+
});
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
finally {
|
|
2413
|
+
resetFailureInjector?.();
|
|
2414
|
+
resetPlatform();
|
|
2415
|
+
await rm(wd, { recursive: true, force: true });
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
it("rolls back Windows plugin-transition artifacts in exact reverse order", async () => {
|
|
2419
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-rollback-"));
|
|
2420
|
+
const forwardOrder = [];
|
|
2421
|
+
const restorationOrder = [];
|
|
2422
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
2423
|
+
let resetFailureInjector;
|
|
2424
|
+
try {
|
|
2425
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2426
|
+
await withTempCwd(wd, async () => {
|
|
2427
|
+
await setup({
|
|
2428
|
+
scope: "user",
|
|
2429
|
+
installMode: "legacy",
|
|
2430
|
+
skipNativeAgentRefresh: true,
|
|
2431
|
+
});
|
|
2432
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2433
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
2434
|
+
const hooksBefore = await readFile(hooksPath);
|
|
2435
|
+
const shimBefore = await readFile(shimPath);
|
|
2436
|
+
const configBefore = await readFile(join(codexHomeDir, "config.toml"));
|
|
2437
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
2438
|
+
if (stage === "before_temp_write")
|
|
2439
|
+
forwardOrder.push(artifact.kind);
|
|
2440
|
+
if (stage === "before_readback" && artifact.kind === "config") {
|
|
2441
|
+
throw new Error("injected Windows config replacement failure");
|
|
2442
|
+
}
|
|
2443
|
+
if (stage === "before_rollback_rename") {
|
|
2444
|
+
restorationOrder.push(artifact.kind);
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
await assert.rejects(setup({
|
|
2448
|
+
scope: "user",
|
|
2449
|
+
installMode: "plugin",
|
|
2450
|
+
skipNativeAgentRefresh: true,
|
|
2451
|
+
pluginAgentsMdPrompt: async () => false,
|
|
2452
|
+
codexFeaturesProbe: () => [
|
|
2453
|
+
"hooks stable true",
|
|
2454
|
+
"plugin_hooks experimental true",
|
|
2455
|
+
"",
|
|
2456
|
+
].join("\n"),
|
|
2457
|
+
}), /rolled back: injected Windows config replacement failure/);
|
|
2458
|
+
assert.deepEqual(forwardOrder, ["hooks", "shim", "config"]);
|
|
2459
|
+
assert.deepEqual(restorationOrder, ["config", "shim", "hooks"]);
|
|
2460
|
+
assert.deepEqual(await readFile(join(codexHomeDir, "config.toml")), configBefore);
|
|
2461
|
+
assert.deepEqual(await readFile(shimPath), shimBefore);
|
|
2462
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
2463
|
+
});
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
finally {
|
|
2467
|
+
resetFailureInjector?.();
|
|
2468
|
+
resetPlatform();
|
|
2469
|
+
await rm(wd, { recursive: true, force: true });
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
it("preserves foreign native hook enablement when transitioning from plugin fallback", async () => {
|
|
2473
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
2474
|
+
try {
|
|
2475
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2476
|
+
await withTempCwd(wd, async () => {
|
|
2477
|
+
await setup({
|
|
2478
|
+
scope: "user",
|
|
2479
|
+
installMode: "plugin",
|
|
2480
|
+
codexFeaturesProbe: () => "hooks stable true\n",
|
|
2481
|
+
});
|
|
2482
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2483
|
+
const existingHooks = JSON.parse(await readFile(hooksPath, "utf-8"));
|
|
2484
|
+
const foreignHookGroup = {
|
|
2485
|
+
hooks: [
|
|
2486
|
+
{
|
|
2487
|
+
type: "command",
|
|
2488
|
+
command: "/usr/bin/python3 /tmp/foreign-hook.py",
|
|
2489
|
+
timeout: 5,
|
|
2490
|
+
},
|
|
2491
|
+
],
|
|
2492
|
+
};
|
|
2493
|
+
const existingRegistrations = existingHooks.hooks ?? {};
|
|
2494
|
+
existingHooks.hooks = {
|
|
2495
|
+
...existingRegistrations,
|
|
2496
|
+
UserPromptSubmit: [
|
|
2497
|
+
foreignHookGroup,
|
|
2498
|
+
...(existingRegistrations.UserPromptSubmit ?? []),
|
|
2499
|
+
],
|
|
2500
|
+
};
|
|
2501
|
+
await writeFile(hooksPath, JSON.stringify(existingHooks, null, 2) + "\n");
|
|
2502
|
+
await setup({ scope: "user", installMode: "plugin" });
|
|
2503
|
+
const finalHooksContent = await readFile(hooksPath, "utf-8");
|
|
2504
|
+
const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
|
|
2505
|
+
assert.match(finalHooksContent, /foreign-hook\.py/);
|
|
2506
|
+
assert.doesNotMatch(finalHooksContent, /codex-native-hook\.js/);
|
|
2507
|
+
assert.match(config, /^plugin_hooks = true$/m);
|
|
2508
|
+
assert.match(config, /^hooks = true$/m);
|
|
2509
|
+
});
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
finally {
|
|
2513
|
+
await rm(wd, { recursive: true, force: true });
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
it("keeps native hooks enabled for pre-existing foreign hooks in plugin-scoped setup", async () => {
|
|
1738
2517
|
const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
|
|
1739
2518
|
try {
|
|
1740
2519
|
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
1741
2520
|
await withTempCwd(wd, async () => {
|
|
1742
2521
|
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
1743
|
-
const
|
|
1744
|
-
|
|
2522
|
+
const foreignHookGroup = {
|
|
2523
|
+
hooks: [
|
|
2524
|
+
{
|
|
2525
|
+
type: "command",
|
|
2526
|
+
command: "/usr/bin/python3 /tmp/foreign-hook.py",
|
|
2527
|
+
timeout: 5,
|
|
2528
|
+
},
|
|
2529
|
+
],
|
|
2530
|
+
};
|
|
2531
|
+
await writeFile(hooksPath, JSON.stringify({ hooks: { UserPromptSubmit: [foreignHookGroup] } }, null, 2) + "\n");
|
|
1745
2532
|
await setup({ scope: "user", installMode: "plugin" });
|
|
1746
|
-
const
|
|
1747
|
-
assert.match(
|
|
1748
|
-
assert.
|
|
2533
|
+
const finalHooksContent = await readFile(hooksPath, "utf-8");
|
|
2534
|
+
assert.match(finalHooksContent, /"UserPromptSubmit"/);
|
|
2535
|
+
assert.match(finalHooksContent, /foreign-hook\.py/);
|
|
2536
|
+
assert.doesNotMatch(finalHooksContent, /codex-native-hook\.js/);
|
|
1749
2537
|
const config = await readFile(join(codexHomeDir, "config.toml"), "utf-8");
|
|
1750
2538
|
assert.match(config, /^plugin_hooks = true$/m);
|
|
2539
|
+
assert.match(config, /^hooks = true$/m, "plugin-scoped setup must keep native hooks enabled for foreign hooks");
|
|
1751
2540
|
});
|
|
1752
2541
|
});
|
|
1753
2542
|
}
|
|
@@ -1940,7 +2729,7 @@ describe("omx setup install mode behavior", () => {
|
|
|
1940
2729
|
assert.match(pluginOutput, /Using setup install mode: plugin/);
|
|
1941
2730
|
assert.match(pluginOutput, /Plugin-scoped Codex hooks and runtime feature flags refresh complete .*plugin_hooks, goals/);
|
|
1942
2731
|
assert.doesNotMatch(pluginOutput, /user-scope skill delivery mode/);
|
|
1943
|
-
assert.doesNotMatch(pluginOutput, /
|
|
2732
|
+
assert.doesNotMatch(pluginOutput, /use explicit agent_type when spawning OMX roles/);
|
|
1944
2733
|
assert.doesNotMatch(pluginOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
|
|
1945
2734
|
assert.doesNotMatch(pluginOutput, /AGENTS keyword routing can also activate them implicitly/);
|
|
1946
2735
|
assert.doesNotMatch(pluginOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
|
|
@@ -1953,7 +2742,7 @@ describe("omx setup install mode behavior", () => {
|
|
|
1953
2742
|
const legacyOutput = await captureConsoleOutput(async () => {
|
|
1954
2743
|
await setup({ scope: "user", installMode: "legacy" });
|
|
1955
2744
|
});
|
|
1956
|
-
assert.match(legacyOutput, /Native agent
|
|
2745
|
+
assert.match(legacyOutput, /Native agent role TOML files written to \.codex\/agents\//);
|
|
1957
2746
|
assert.match(legacyOutput, /Use role\/workflow keywords like \$architect, \$executor, and \$plan/);
|
|
1958
2747
|
assert.match(legacyOutput, /AGENTS keyword routing can also activate them implicitly/);
|
|
1959
2748
|
assert.match(legacyOutput, /The AGENTS\.md orchestration brain is loaded automatically/);
|
|
@@ -2198,5 +2987,1463 @@ describe("omx setup install mode behavior", () => {
|
|
|
2198
2987
|
await rm(wd, { recursive: true, force: true });
|
|
2199
2988
|
}
|
|
2200
2989
|
});
|
|
2990
|
+
it("fails plugin hook-removal preflight before creating unrelated setup artifacts", async () => {
|
|
2991
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-preflight-"));
|
|
2992
|
+
try {
|
|
2993
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
2994
|
+
await withTempCwd(wd, async () => {
|
|
2995
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
2996
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
2997
|
+
const hooksContent = JSON.stringify({
|
|
2998
|
+
hooks: {
|
|
2999
|
+
SessionStart: [
|
|
3000
|
+
{
|
|
3001
|
+
matcher: "startup|resume|clear",
|
|
3002
|
+
hooks: [
|
|
3003
|
+
{
|
|
3004
|
+
type: "command",
|
|
3005
|
+
command: 'node "/repo/dist/scripts/codex-native-hook.js"',
|
|
3006
|
+
},
|
|
3007
|
+
],
|
|
3008
|
+
},
|
|
3009
|
+
{ hooks: [{ type: "command", command: "echo foreign" }] },
|
|
3010
|
+
],
|
|
3011
|
+
},
|
|
3012
|
+
}, null, 2) + "\n";
|
|
3013
|
+
const configContent = 'model = "foreign-config"\n';
|
|
3014
|
+
await writeFile(hooksPath, hooksContent);
|
|
3015
|
+
await writeFile(configPath, configContent);
|
|
3016
|
+
await assert.rejects(() => setup({
|
|
3017
|
+
scope: "user",
|
|
3018
|
+
installMode: "plugin",
|
|
3019
|
+
mergeAgents: true,
|
|
3020
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
3021
|
+
}), (error) => {
|
|
3022
|
+
assert.equal(error.code, "unsafe_managed_removal");
|
|
3023
|
+
return true;
|
|
3024
|
+
});
|
|
3025
|
+
assert.equal(await readFile(hooksPath, "utf-8"), hooksContent);
|
|
3026
|
+
assert.equal(await readFile(configPath, "utf-8"), configContent);
|
|
3027
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3028
|
+
assert.equal(existsSync(join(codexHomeDir, "prompts")), false);
|
|
3029
|
+
assert.equal(existsSync(join(codexHomeDir, "agents")), false);
|
|
3030
|
+
});
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
finally {
|
|
3034
|
+
await rm(wd, { recursive: true, force: true });
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
it("rejects symlinked Codex transaction ancestors without writing foreign storage", async () => {
|
|
3038
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-symlinked-codex-home-"));
|
|
3039
|
+
try {
|
|
3040
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3041
|
+
await withTempCwd(wd, async () => {
|
|
3042
|
+
const foreignCodexHome = join(wd, "foreign-codex-home");
|
|
3043
|
+
await mkdir(foreignCodexHome);
|
|
3044
|
+
await writeFile(join(foreignCodexHome, "sentinel.txt"), "foreign\n");
|
|
3045
|
+
await rm(codexHomeDir, { recursive: true, force: true });
|
|
3046
|
+
await symlink(foreignCodexHome, codexHomeDir);
|
|
3047
|
+
await assert.rejects(setup({
|
|
3048
|
+
scope: "user",
|
|
3049
|
+
installMode: "legacy",
|
|
3050
|
+
skipNativeAgentRefresh: true,
|
|
3051
|
+
}), /ancestor .*symbolic link/);
|
|
3052
|
+
assert.deepEqual(await readdir(foreignCodexHome), ["sentinel.txt"]);
|
|
3053
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3054
|
+
});
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
finally {
|
|
3058
|
+
await rm(wd, { recursive: true, force: true });
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
it("rejects a symlinked native artifact parent before touching its target", async () => {
|
|
3062
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-symlinked-native-parent-"));
|
|
3063
|
+
try {
|
|
3064
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3065
|
+
await withTempCwd(wd, async () => {
|
|
3066
|
+
const foreignMetadataDir = join(wd, "foreign-native-metadata");
|
|
3067
|
+
await mkdir(foreignMetadataDir);
|
|
3068
|
+
await writeFile(join(foreignMetadataDir, "sentinel.txt"), "foreign\n");
|
|
3069
|
+
await symlink(foreignMetadataDir, join(codexHomeDir, ".omx"));
|
|
3070
|
+
await assert.rejects(setup({
|
|
3071
|
+
scope: "user",
|
|
3072
|
+
installMode: "legacy",
|
|
3073
|
+
skipNativeAgentRefresh: true,
|
|
3074
|
+
}), /ancestor .*symbolic link/);
|
|
3075
|
+
assert.deepEqual(await readdir(foreignMetadataDir), ["sentinel.txt"]);
|
|
3076
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
|
|
3077
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3078
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3079
|
+
});
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
finally {
|
|
3083
|
+
await rm(wd, { recursive: true, force: true });
|
|
3084
|
+
}
|
|
3085
|
+
});
|
|
3086
|
+
it("revalidates native artifact parent topology immediately before mutation", async () => {
|
|
3087
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-native-parent-"));
|
|
3088
|
+
let resetFailureInjector;
|
|
3089
|
+
try {
|
|
3090
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3091
|
+
await withTempCwd(wd, async () => {
|
|
3092
|
+
const metadataParent = join(codexHomeDir, ".omx");
|
|
3093
|
+
let injected = false;
|
|
3094
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
3095
|
+
if (stage !== "before_precondition" || injected)
|
|
3096
|
+
return;
|
|
3097
|
+
injected = true;
|
|
3098
|
+
writeFileSync(metadataParent, "foreign\n");
|
|
3099
|
+
});
|
|
3100
|
+
await assert.rejects(setup({
|
|
3101
|
+
scope: "user",
|
|
3102
|
+
installMode: "legacy",
|
|
3103
|
+
skipNativeAgentRefresh: true,
|
|
3104
|
+
}), /ancestor .*not a directory/);
|
|
3105
|
+
assert.equal(injected, true);
|
|
3106
|
+
assert.equal(await readFile(metadataParent, "utf-8"), "foreign\n");
|
|
3107
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
|
|
3108
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3109
|
+
});
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
finally {
|
|
3113
|
+
resetFailureInjector?.();
|
|
3114
|
+
await rm(wd, { recursive: true, force: true });
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
it("rejects a modified Windows hook shim during setup preflight", async () => {
|
|
3118
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-shim-"));
|
|
3119
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3120
|
+
try {
|
|
3121
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3122
|
+
await withTempCwd(wd, async () => {
|
|
3123
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3124
|
+
const modifiedShim = "# modified by user\n";
|
|
3125
|
+
await writeFile(shimPath, modifiedShim);
|
|
3126
|
+
await assert.rejects(() => setup({
|
|
3127
|
+
scope: "user",
|
|
3128
|
+
installMode: "legacy",
|
|
3129
|
+
mergeAgents: true,
|
|
3130
|
+
}), /modified Windows native hook shim/);
|
|
3131
|
+
assert.equal(await readFile(shimPath, "utf-8"), modifiedShim);
|
|
3132
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3133
|
+
assert.equal(existsSync(join(codexHomeDir, "config.toml")), false);
|
|
3134
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3135
|
+
});
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
finally {
|
|
3139
|
+
resetPlatform();
|
|
3140
|
+
await rm(wd, { recursive: true, force: true });
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
it("writes hooks, notification metadata, then config/trust and rolls all writes back when config replacement fails", async () => {
|
|
3144
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-hook-transaction-"));
|
|
3145
|
+
const writeOrder = [];
|
|
3146
|
+
const resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
3147
|
+
if (stage !== "before_temp_write")
|
|
3148
|
+
return;
|
|
3149
|
+
writeOrder.push(artifact.kind);
|
|
3150
|
+
if (artifact.kind === "config") {
|
|
3151
|
+
throw new Error("injected config replacement failure");
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
try {
|
|
3155
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3156
|
+
await withTempCwd(wd, async () => {
|
|
3157
|
+
await writeFile(join(codexHomeDir, "config.toml"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3158
|
+
await assert.rejects(() => setup({
|
|
3159
|
+
scope: "user",
|
|
3160
|
+
installMode: "legacy",
|
|
3161
|
+
mergeAgents: true,
|
|
3162
|
+
skipNativeAgentRefresh: true,
|
|
3163
|
+
}), /rolled back: injected config replacement failure/);
|
|
3164
|
+
assert.deepEqual(writeOrder, ["hooks", "metadata", "config"]);
|
|
3165
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3166
|
+
assert.equal(await readFile(join(codexHomeDir, "config.toml"), "utf-8"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3167
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false);
|
|
3168
|
+
});
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
finally {
|
|
3172
|
+
resetFailureInjector();
|
|
3173
|
+
await rm(wd, { recursive: true, force: true });
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
it("writes the Windows shim, hooks, notification metadata, then config/trust and rolls all writes back", async () => {
|
|
3177
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-windows-hook-transaction-"));
|
|
3178
|
+
const writeOrder = [];
|
|
3179
|
+
const resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, artifact) => {
|
|
3180
|
+
if (stage !== "before_temp_write")
|
|
3181
|
+
return;
|
|
3182
|
+
writeOrder.push(artifact.kind);
|
|
3183
|
+
if (artifact.kind === "config") {
|
|
3184
|
+
throw new Error("injected Windows config replacement failure");
|
|
3185
|
+
}
|
|
3186
|
+
});
|
|
3187
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3188
|
+
try {
|
|
3189
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3190
|
+
await withTempCwd(wd, async () => {
|
|
3191
|
+
await writeFile(join(codexHomeDir, "config.toml"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3192
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3193
|
+
await assert.rejects(() => setup({
|
|
3194
|
+
scope: "user",
|
|
3195
|
+
installMode: "legacy",
|
|
3196
|
+
mergeAgents: true,
|
|
3197
|
+
skipNativeAgentRefresh: true,
|
|
3198
|
+
}), /rolled back: injected Windows config replacement failure/);
|
|
3199
|
+
assert.deepEqual(writeOrder, ["shim", "hooks", "metadata", "config"]);
|
|
3200
|
+
assert.equal(existsSync(shimPath), false);
|
|
3201
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3202
|
+
assert.equal(await readFile(join(codexHomeDir, "config.toml"), "utf-8"), 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3203
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false);
|
|
3204
|
+
});
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
finally {
|
|
3208
|
+
resetFailureInjector();
|
|
3209
|
+
resetPlatform();
|
|
3210
|
+
await rm(wd, { recursive: true, force: true });
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
3213
|
+
it("aborts stale native snapshots before setup can write unrelated artifacts", async () => {
|
|
3214
|
+
const originalConfig = Buffer.from('model = "before"\n', "utf-8");
|
|
3215
|
+
const originalHooks = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3216
|
+
const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
|
|
3217
|
+
const foreignConfig = Buffer.from('model = "foreign"\n', "utf-8");
|
|
3218
|
+
const fixtures = [
|
|
3219
|
+
{
|
|
3220
|
+
name: "hooks creation",
|
|
3221
|
+
seedHooks: null,
|
|
3222
|
+
mutate: (hooksPath, _configPath) => writeFileSync(hooksPath, foreignHooks),
|
|
3223
|
+
expectedHooks: foreignHooks,
|
|
3224
|
+
expectedConfig: originalConfig,
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
name: "config modification",
|
|
3228
|
+
seedHooks: originalHooks,
|
|
3229
|
+
mutate: (_hooksPath, configPath) => writeFileSync(configPath, foreignConfig),
|
|
3230
|
+
expectedHooks: originalHooks,
|
|
3231
|
+
expectedConfig: foreignConfig,
|
|
3232
|
+
},
|
|
3233
|
+
{
|
|
3234
|
+
name: "hooks deletion",
|
|
3235
|
+
seedHooks: originalHooks,
|
|
3236
|
+
mutate: (hooksPath, _configPath) => rmSync(hooksPath),
|
|
3237
|
+
expectedHooks: null,
|
|
3238
|
+
expectedConfig: originalConfig,
|
|
3239
|
+
},
|
|
3240
|
+
{
|
|
3241
|
+
name: "config deletion",
|
|
3242
|
+
seedHooks: originalHooks,
|
|
3243
|
+
mutate: (_hooksPath, configPath) => rmSync(configPath),
|
|
3244
|
+
expectedHooks: originalHooks,
|
|
3245
|
+
expectedConfig: null,
|
|
3246
|
+
},
|
|
3247
|
+
];
|
|
3248
|
+
for (const fixture of fixtures) {
|
|
3249
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-native-"));
|
|
3250
|
+
let resetFailureInjector;
|
|
3251
|
+
try {
|
|
3252
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3253
|
+
await withTempCwd(wd, async () => {
|
|
3254
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3255
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3256
|
+
await writeFile(configPath, originalConfig);
|
|
3257
|
+
if (fixture.seedHooks)
|
|
3258
|
+
await writeFile(hooksPath, fixture.seedHooks);
|
|
3259
|
+
let injected = false;
|
|
3260
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
3261
|
+
if (stage !== "before_precondition" || injected)
|
|
3262
|
+
return;
|
|
3263
|
+
injected = true;
|
|
3264
|
+
fixture.mutate(hooksPath, configPath);
|
|
3265
|
+
});
|
|
3266
|
+
await assert.rejects(setup({
|
|
3267
|
+
scope: "user",
|
|
3268
|
+
installMode: "legacy",
|
|
3269
|
+
skipNativeAgentRefresh: true,
|
|
3270
|
+
codexFeaturesProbe: () => null,
|
|
3271
|
+
codexVersionProbe: () => null,
|
|
3272
|
+
}), /precondition changed/);
|
|
3273
|
+
assert.equal(injected, true, fixture.name);
|
|
3274
|
+
if (fixture.expectedConfig === null) {
|
|
3275
|
+
assert.equal(existsSync(configPath), false);
|
|
3276
|
+
}
|
|
3277
|
+
else {
|
|
3278
|
+
assert.deepEqual(await readFile(configPath), fixture.expectedConfig);
|
|
3279
|
+
}
|
|
3280
|
+
if (fixture.expectedHooks === null) {
|
|
3281
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3282
|
+
}
|
|
3283
|
+
else {
|
|
3284
|
+
assert.deepEqual(await readFile(hooksPath), fixture.expectedHooks);
|
|
3285
|
+
}
|
|
3286
|
+
assert.equal(existsSync(join(wd, ".omx")), false);
|
|
3287
|
+
assert.equal(existsSync(join(codexHomeDir, "agents")), false);
|
|
3288
|
+
});
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
finally {
|
|
3292
|
+
resetFailureInjector?.();
|
|
3293
|
+
await rm(wd, { recursive: true, force: true });
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
it("aborts when notification metadata changes after planning", async () => {
|
|
3298
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-notify-metadata-"));
|
|
3299
|
+
let resetFailureInjector;
|
|
3300
|
+
try {
|
|
3301
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3302
|
+
await withTempCwd(wd, async () => {
|
|
3303
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3304
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3305
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
3306
|
+
const configBefore = 'notify = ["node", "/tmp/user-notify.js"]\n';
|
|
3307
|
+
const foreignMetadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
3308
|
+
await writeFile(configPath, configBefore);
|
|
3309
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
3310
|
+
let injected = false;
|
|
3311
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3312
|
+
if (stage !== "before_precondition" ||
|
|
3313
|
+
target.kind !== "metadata" ||
|
|
3314
|
+
injected) {
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
injected = true;
|
|
3318
|
+
writeFileSync(metadataPath, foreignMetadata);
|
|
3319
|
+
});
|
|
3320
|
+
await assert.rejects(setup({
|
|
3321
|
+
scope: "user",
|
|
3322
|
+
installMode: "legacy",
|
|
3323
|
+
skipNativeAgentRefresh: true,
|
|
3324
|
+
codexFeaturesProbe: () => null,
|
|
3325
|
+
codexVersionProbe: () => null,
|
|
3326
|
+
}), /precondition changed/);
|
|
3327
|
+
assert.equal(injected, true);
|
|
3328
|
+
assert.deepEqual(await readFile(metadataPath), foreignMetadata);
|
|
3329
|
+
assert.equal(await readFile(configPath, "utf-8"), configBefore);
|
|
3330
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3331
|
+
});
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
finally {
|
|
3335
|
+
resetFailureInjector?.();
|
|
3336
|
+
await rm(wd, { recursive: true, force: true });
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
it("aborts when the Windows shim changes after planning", async () => {
|
|
3340
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-windows-shim-"));
|
|
3341
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3342
|
+
let resetFailureInjector;
|
|
3343
|
+
try {
|
|
3344
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3345
|
+
await withTempCwd(wd, async () => {
|
|
3346
|
+
await setup({
|
|
3347
|
+
scope: "user",
|
|
3348
|
+
installMode: "legacy",
|
|
3349
|
+
skipNativeAgentRefresh: true,
|
|
3350
|
+
codexFeaturesProbe: () => null,
|
|
3351
|
+
codexVersionProbe: () => null,
|
|
3352
|
+
});
|
|
3353
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3354
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3355
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3356
|
+
const configBefore = await readFile(configPath);
|
|
3357
|
+
await writeFile(hooksPath, '{"hooks": {}}\n');
|
|
3358
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3359
|
+
const foreignShim = Buffer.from("# foreign shim\n", "utf-8");
|
|
3360
|
+
let injected = false;
|
|
3361
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3362
|
+
if (stage === "before_precondition" &&
|
|
3363
|
+
target.kind === "shim" &&
|
|
3364
|
+
!injected) {
|
|
3365
|
+
injected = true;
|
|
3366
|
+
writeFileSync(shimPath, foreignShim);
|
|
3367
|
+
}
|
|
3368
|
+
});
|
|
3369
|
+
await assert.rejects(setup({
|
|
3370
|
+
scope: "user",
|
|
3371
|
+
installMode: "legacy",
|
|
3372
|
+
skipNativeAgentRefresh: true,
|
|
3373
|
+
codexFeaturesProbe: () => null,
|
|
3374
|
+
codexVersionProbe: () => null,
|
|
3375
|
+
}), /precondition changed/);
|
|
3376
|
+
assert.equal(injected, true);
|
|
3377
|
+
assert.deepEqual(await readFile(shimPath), foreignShim);
|
|
3378
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3379
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3380
|
+
});
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
finally {
|
|
3384
|
+
resetFailureInjector?.();
|
|
3385
|
+
resetPlatform();
|
|
3386
|
+
await rm(wd, { recursive: true, force: true });
|
|
3387
|
+
}
|
|
3388
|
+
});
|
|
3389
|
+
it("never deletes plugin hooks from a stale snapshot", async () => {
|
|
3390
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-plugin-stale-hooks-"));
|
|
3391
|
+
let resetFailureInjector;
|
|
3392
|
+
try {
|
|
3393
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3394
|
+
await withTempCwd(wd, async () => {
|
|
3395
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
3396
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3397
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3398
|
+
const configBefore = await readFile(configPath);
|
|
3399
|
+
const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
|
|
3400
|
+
let injected = false;
|
|
3401
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage) => {
|
|
3402
|
+
if (stage !== "before_precondition" || injected)
|
|
3403
|
+
return;
|
|
3404
|
+
injected = true;
|
|
3405
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3406
|
+
});
|
|
3407
|
+
await assert.rejects(setup({
|
|
3408
|
+
scope: "user",
|
|
3409
|
+
installMode: "plugin",
|
|
3410
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3411
|
+
skipNativeAgentRefresh: true,
|
|
3412
|
+
}), /precondition changed/);
|
|
3413
|
+
assert.equal(injected, true);
|
|
3414
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3415
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3416
|
+
});
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
finally {
|
|
3420
|
+
resetFailureInjector?.();
|
|
3421
|
+
await rm(wd, { recursive: true, force: true });
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
it("never rolls back a concurrent foreign hook replacement", async () => {
|
|
3425
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-rollback-"));
|
|
3426
|
+
let resetFailureInjector;
|
|
3427
|
+
try {
|
|
3428
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3429
|
+
await withTempCwd(wd, async () => {
|
|
3430
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3431
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3432
|
+
const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3433
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
3434
|
+
const foreignHooks = Buffer.from('{"hooks": {"Stop": []}}\n', "utf-8");
|
|
3435
|
+
await writeFile(hooksPath, hooksBefore);
|
|
3436
|
+
await writeFile(configPath, configBefore);
|
|
3437
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3438
|
+
if (stage !== "before_temp_write" || target.kind !== "config")
|
|
3439
|
+
return;
|
|
3440
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3441
|
+
throw new Error("injected config replacement failure after foreign hook write");
|
|
3442
|
+
});
|
|
3443
|
+
await assert.rejects(setup({
|
|
3444
|
+
scope: "user",
|
|
3445
|
+
installMode: "legacy",
|
|
3446
|
+
skipNativeAgentRefresh: true,
|
|
3447
|
+
codexFeaturesProbe: () => null,
|
|
3448
|
+
codexVersionProbe: () => null,
|
|
3449
|
+
}), /rollback failed/);
|
|
3450
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3451
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3452
|
+
});
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
finally {
|
|
3456
|
+
resetFailureInjector?.();
|
|
3457
|
+
await rm(wd, { recursive: true, force: true });
|
|
3458
|
+
}
|
|
3459
|
+
});
|
|
3460
|
+
it("preserves pre-existing native bytes and modes across injected transaction failures", async () => {
|
|
3461
|
+
const failures = [
|
|
3462
|
+
{ stage: "before_backup", kind: "shim" },
|
|
3463
|
+
{ stage: "before_temp_write", kind: "shim" },
|
|
3464
|
+
{ stage: "before_rename", kind: "hooks" },
|
|
3465
|
+
{ stage: "before_temp_write", kind: "metadata" },
|
|
3466
|
+
{ stage: "before_readback", kind: "config" },
|
|
3467
|
+
];
|
|
3468
|
+
for (const failure of failures) {
|
|
3469
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-transaction-rollback-"));
|
|
3470
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3471
|
+
let resetFailureInjector;
|
|
3472
|
+
try {
|
|
3473
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3474
|
+
await withTempCwd(wd, async () => {
|
|
3475
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3476
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3477
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3478
|
+
const shimBefore = Buffer.from(buildManagedCodexNativeHookWindowsShimContent("", {
|
|
3479
|
+
nodePath: "C:\\Historical Node\\node.exe",
|
|
3480
|
+
hookScriptPath: "C:\\Historical Install\\oh-my-codex\\dist\\scripts\\codex-native-hook.js",
|
|
3481
|
+
}), "utf-8");
|
|
3482
|
+
const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3483
|
+
const configBefore = Buffer.from('model = "before"\nnotify = ["node", "/tmp/user-notify.js"]\n', "utf-8");
|
|
3484
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
3485
|
+
await writeFile(shimPath, shimBefore);
|
|
3486
|
+
await writeFile(hooksPath, hooksBefore);
|
|
3487
|
+
await writeFile(configPath, configBefore);
|
|
3488
|
+
await Promise.all([
|
|
3489
|
+
chmod(shimPath, 0o640),
|
|
3490
|
+
chmod(hooksPath, 0o640),
|
|
3491
|
+
chmod(configPath, 0o640),
|
|
3492
|
+
]);
|
|
3493
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3494
|
+
if (stage === failure.stage && target.kind === failure.kind) {
|
|
3495
|
+
throw new Error(`injected ${failure.stage} ${failure.kind} failure`);
|
|
3496
|
+
}
|
|
3497
|
+
});
|
|
3498
|
+
await assert.rejects(setup({
|
|
3499
|
+
scope: "user",
|
|
3500
|
+
installMode: "legacy",
|
|
3501
|
+
skipNativeAgentRefresh: true,
|
|
3502
|
+
codexFeaturesProbe: () => null,
|
|
3503
|
+
codexVersionProbe: () => null,
|
|
3504
|
+
}), /injected .* failure/);
|
|
3505
|
+
assert.deepEqual(await readFile(shimPath), shimBefore, failure.stage);
|
|
3506
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore, failure.stage);
|
|
3507
|
+
assert.deepEqual(await readFile(configPath), configBefore, failure.stage);
|
|
3508
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx", "notify-dispatch.json")), false, failure.stage);
|
|
3509
|
+
for (const path of [shimPath, hooksPath, configPath]) {
|
|
3510
|
+
assert.equal((await stat(path)).mode & 0o777, 0o640, path);
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
finally {
|
|
3516
|
+
resetFailureInjector?.();
|
|
3517
|
+
resetPlatform();
|
|
3518
|
+
await rm(wd, { recursive: true, force: true });
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
it("fails closed when a managed dispatcher metadata snapshot is not valid UTF-8", async () => {
|
|
3523
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-invalid-notify-metadata-"));
|
|
3524
|
+
try {
|
|
3525
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3526
|
+
await withTempCwd(wd, async () => {
|
|
3527
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3528
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
3529
|
+
const dispatcherPath = join(packageRoot, "dist", "scripts", "notify-dispatcher.js");
|
|
3530
|
+
const configBefore = `notify = ${JSON.stringify([
|
|
3531
|
+
"node",
|
|
3532
|
+
dispatcherPath,
|
|
3533
|
+
"--metadata",
|
|
3534
|
+
metadataPath,
|
|
3535
|
+
])}\n`;
|
|
3536
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
3537
|
+
await writeFile(configPath, configBefore);
|
|
3538
|
+
await writeFile(metadataPath, Buffer.from([0xff]));
|
|
3539
|
+
await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /notification metadata .*invalid UTF-8/);
|
|
3540
|
+
assert.equal(await readFile(configPath, "utf-8"), configBefore);
|
|
3541
|
+
assert.deepEqual(await readFile(metadataPath), Buffer.from([0xff]));
|
|
3542
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3543
|
+
});
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
finally {
|
|
3547
|
+
await rm(wd, { recursive: true, force: true });
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
it("keeps consulted unchanged notification metadata in the native transaction preconditions", async () => {
|
|
3551
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-notify-precondition-"));
|
|
3552
|
+
let resetFailureInjector;
|
|
3553
|
+
try {
|
|
3554
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3555
|
+
await withTempCwd(wd, async () => {
|
|
3556
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3557
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3558
|
+
const metadataPath = join(codexHomeDir, ".omx", "notify-dispatch.json");
|
|
3559
|
+
await writeFile(configPath, 'notify = ["node", "/tmp/user-notify.js"]\n');
|
|
3560
|
+
await setup({
|
|
3561
|
+
scope: "user",
|
|
3562
|
+
installMode: "legacy",
|
|
3563
|
+
skipNativeAgentRefresh: true,
|
|
3564
|
+
});
|
|
3565
|
+
const configBefore = await readFile(configPath);
|
|
3566
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3567
|
+
const foreignMetadata = Buffer.from('{"managedBy":"foreign"}\n', "utf-8");
|
|
3568
|
+
let injected = false;
|
|
3569
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3570
|
+
if (stage === "before_precondition" &&
|
|
3571
|
+
target.kind === "metadata" &&
|
|
3572
|
+
!injected) {
|
|
3573
|
+
injected = true;
|
|
3574
|
+
writeFileSync(metadataPath, foreignMetadata);
|
|
3575
|
+
}
|
|
3576
|
+
});
|
|
3577
|
+
await assert.rejects(setup({
|
|
3578
|
+
scope: "user",
|
|
3579
|
+
installMode: "legacy",
|
|
3580
|
+
skipNativeAgentRefresh: true,
|
|
3581
|
+
}), /precondition changed/);
|
|
3582
|
+
assert.equal(injected, true);
|
|
3583
|
+
assert.deepEqual(await readFile(metadataPath), foreignMetadata);
|
|
3584
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3585
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3586
|
+
});
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
finally {
|
|
3590
|
+
resetFailureInjector?.();
|
|
3591
|
+
await rm(wd, { recursive: true, force: true });
|
|
3592
|
+
}
|
|
3593
|
+
});
|
|
3594
|
+
it("rejects config decisions whose MCP-removal callback changed the planned config snapshot", async () => {
|
|
3595
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-stale-config-decision-"));
|
|
3596
|
+
try {
|
|
3597
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3598
|
+
await withTempCwd(wd, async () => {
|
|
3599
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3600
|
+
const configBefore = '[mcp_servers.omx_state]\ncommand = "node"\n';
|
|
3601
|
+
const foreignConfig = 'model = "foreign"\n';
|
|
3602
|
+
await writeFile(configPath, configBefore);
|
|
3603
|
+
await assert.rejects(setup({
|
|
3604
|
+
scope: "user",
|
|
3605
|
+
installMode: "legacy",
|
|
3606
|
+
skipNativeAgentRefresh: true,
|
|
3607
|
+
firstPartyMcpRemovalPrompt: async () => {
|
|
3608
|
+
await writeFile(configPath, foreignConfig);
|
|
3609
|
+
return false;
|
|
3610
|
+
},
|
|
3611
|
+
}), /precondition changed/);
|
|
3612
|
+
assert.equal(await readFile(configPath, "utf-8"), foreignConfig);
|
|
3613
|
+
assert.equal(existsSync(join(codexHomeDir, "hooks.json")), false);
|
|
3614
|
+
assert.equal(existsSync(join(codexHomeDir, ".omx")), false);
|
|
3615
|
+
});
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
finally {
|
|
3619
|
+
await rm(wd, { recursive: true, force: true });
|
|
3620
|
+
}
|
|
3621
|
+
});
|
|
3622
|
+
it("fails closed for pre-existing native transaction write temporaries", async () => {
|
|
3623
|
+
const fixtures = ["regular file", "symlink"];
|
|
3624
|
+
for (const fixture of fixtures) {
|
|
3625
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-temp-collision-"));
|
|
3626
|
+
let resetTemporaryPath;
|
|
3627
|
+
try {
|
|
3628
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3629
|
+
await withTempCwd(wd, async () => {
|
|
3630
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3631
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3632
|
+
const collisionPath = join(codexHomeDir, ".hooks-write-collision");
|
|
3633
|
+
const configBefore = 'model = "before"\n';
|
|
3634
|
+
await writeFile(configPath, configBefore);
|
|
3635
|
+
if (fixture === "regular file") {
|
|
3636
|
+
await writeFile(collisionPath, "collision\n");
|
|
3637
|
+
}
|
|
3638
|
+
else {
|
|
3639
|
+
const targetPath = join(wd, "collision-target");
|
|
3640
|
+
await writeFile(targetPath, "collision\n");
|
|
3641
|
+
await symlink(targetPath, collisionPath);
|
|
3642
|
+
}
|
|
3643
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "write"
|
|
3644
|
+
? collisionPath
|
|
3645
|
+
: join(dirname(path), `.${purpose}-unused`));
|
|
3646
|
+
await assert.rejects(setup({
|
|
3647
|
+
scope: "user",
|
|
3648
|
+
installMode: "legacy",
|
|
3649
|
+
skipNativeAgentRefresh: true,
|
|
3650
|
+
}), /EEXIST/);
|
|
3651
|
+
assert.equal(await readFile(collisionPath, "utf-8"), "collision\n");
|
|
3652
|
+
assert.equal(await readFile(configPath, "utf-8"), configBefore);
|
|
3653
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3654
|
+
});
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
finally {
|
|
3658
|
+
resetTemporaryPath?.();
|
|
3659
|
+
await rm(wd, { recursive: true, force: true });
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
});
|
|
3663
|
+
it("fails closed when a non-throwing injector replaces an owned write temporary", async () => {
|
|
3664
|
+
const fixtures = ["regular file", "symlink"];
|
|
3665
|
+
for (const fixture of fixtures) {
|
|
3666
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-write-temp-replacement-"));
|
|
3667
|
+
let resetFailureInjector;
|
|
3668
|
+
let resetTemporaryPath;
|
|
3669
|
+
try {
|
|
3670
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3671
|
+
await withTempCwd(wd, async () => {
|
|
3672
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3673
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3674
|
+
const temporaryPath = join(codexHomeDir, ".hooks-write-owned");
|
|
3675
|
+
const foreignTargetPath = join(wd, "foreign-write-temporary-target");
|
|
3676
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
3677
|
+
const foreignContents = `foreign ${fixture} write temporary\n`;
|
|
3678
|
+
await writeFile(configPath, configBefore);
|
|
3679
|
+
if (fixture === "symlink") {
|
|
3680
|
+
await writeFile(foreignTargetPath, foreignContents);
|
|
3681
|
+
}
|
|
3682
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "write"
|
|
3683
|
+
? temporaryPath
|
|
3684
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
|
|
3685
|
+
let injected = false;
|
|
3686
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3687
|
+
if (stage !== "before_rename" || target.kind !== "hooks")
|
|
3688
|
+
return;
|
|
3689
|
+
injected = true;
|
|
3690
|
+
rmSync(temporaryPath);
|
|
3691
|
+
if (fixture === "regular file") {
|
|
3692
|
+
writeFileSync(temporaryPath, foreignContents);
|
|
3693
|
+
}
|
|
3694
|
+
else {
|
|
3695
|
+
symlinkSync(foreignTargetPath, temporaryPath);
|
|
3696
|
+
}
|
|
3697
|
+
});
|
|
3698
|
+
await assert.rejects(setup({
|
|
3699
|
+
scope: "user",
|
|
3700
|
+
installMode: "legacy",
|
|
3701
|
+
skipNativeAgentRefresh: true,
|
|
3702
|
+
}), /temporary.*manual recovery/);
|
|
3703
|
+
assert.equal(injected, true);
|
|
3704
|
+
const replacementStatus = await lstat(temporaryPath);
|
|
3705
|
+
if (fixture === "regular file") {
|
|
3706
|
+
assert.equal(replacementStatus.isFile(), true);
|
|
3707
|
+
assert.equal(await readFile(temporaryPath, "utf-8"), foreignContents);
|
|
3708
|
+
}
|
|
3709
|
+
else {
|
|
3710
|
+
assert.equal(replacementStatus.isSymbolicLink(), true);
|
|
3711
|
+
assert.equal(await readFile(foreignTargetPath, "utf-8"), foreignContents);
|
|
3712
|
+
}
|
|
3713
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3714
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3715
|
+
});
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
finally {
|
|
3719
|
+
resetFailureInjector?.();
|
|
3720
|
+
resetTemporaryPath?.();
|
|
3721
|
+
await rm(wd, { recursive: true, force: true });
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
});
|
|
3725
|
+
it("fails closed when a non-throwing injector replaces an owned staged deletion", async () => {
|
|
3726
|
+
const fixtures = ["regular file", "symlink"];
|
|
3727
|
+
for (const fixture of fixtures) {
|
|
3728
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-staged-delete-replacement-"));
|
|
3729
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3730
|
+
let resetFailureInjector;
|
|
3731
|
+
let resetTemporaryPath;
|
|
3732
|
+
try {
|
|
3733
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3734
|
+
await withTempCwd(wd, async () => {
|
|
3735
|
+
await setup({
|
|
3736
|
+
scope: "user",
|
|
3737
|
+
installMode: "legacy",
|
|
3738
|
+
skipNativeAgentRefresh: true,
|
|
3739
|
+
});
|
|
3740
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3741
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3742
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3743
|
+
const stagedDeletionPath = join(codexHomeDir, ".hooks-delete-owned");
|
|
3744
|
+
const foreignTargetPath = join(wd, "foreign-staged-deletion-target");
|
|
3745
|
+
const shimBefore = await readFile(shimPath);
|
|
3746
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3747
|
+
const configBefore = await readFile(configPath);
|
|
3748
|
+
const foreignContents = `foreign ${fixture} staged deletion\n`;
|
|
3749
|
+
if (fixture === "symlink") {
|
|
3750
|
+
await writeFile(foreignTargetPath, foreignContents);
|
|
3751
|
+
}
|
|
3752
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
|
|
3753
|
+
? stagedDeletionPath
|
|
3754
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
|
|
3755
|
+
let injected = false;
|
|
3756
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3757
|
+
if (stage !== "before_remove" || target.kind !== "hooks")
|
|
3758
|
+
return;
|
|
3759
|
+
injected = true;
|
|
3760
|
+
rmSync(stagedDeletionPath);
|
|
3761
|
+
if (fixture === "regular file") {
|
|
3762
|
+
writeFileSync(stagedDeletionPath, foreignContents);
|
|
3763
|
+
}
|
|
3764
|
+
else {
|
|
3765
|
+
symlinkSync(foreignTargetPath, stagedDeletionPath);
|
|
3766
|
+
}
|
|
3767
|
+
});
|
|
3768
|
+
await assert.rejects(setup({
|
|
3769
|
+
scope: "user",
|
|
3770
|
+
installMode: "plugin",
|
|
3771
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3772
|
+
skipNativeAgentRefresh: true,
|
|
3773
|
+
}), /staged deletion.*manual recovery/);
|
|
3774
|
+
assert.equal(injected, true);
|
|
3775
|
+
const replacementStatus = await lstat(stagedDeletionPath);
|
|
3776
|
+
if (fixture === "regular file") {
|
|
3777
|
+
assert.equal(replacementStatus.isFile(), true);
|
|
3778
|
+
assert.equal(await readFile(stagedDeletionPath, "utf-8"), foreignContents);
|
|
3779
|
+
}
|
|
3780
|
+
else {
|
|
3781
|
+
assert.equal(replacementStatus.isSymbolicLink(), true);
|
|
3782
|
+
assert.equal(await readFile(foreignTargetPath, "utf-8"), foreignContents);
|
|
3783
|
+
}
|
|
3784
|
+
assert.deepEqual(await readFile(shimPath), shimBefore);
|
|
3785
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3786
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3787
|
+
});
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
finally {
|
|
3791
|
+
resetFailureInjector?.();
|
|
3792
|
+
resetTemporaryPath?.();
|
|
3793
|
+
resetPlatform();
|
|
3794
|
+
await rm(wd, { recursive: true, force: true });
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
});
|
|
3798
|
+
it("rolls back a renamed native hook when post-rename verification fails", async () => {
|
|
3799
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-post-rename-rollback-"));
|
|
3800
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("linux");
|
|
3801
|
+
let resetFailureInjector;
|
|
3802
|
+
try {
|
|
3803
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3804
|
+
await withTempCwd(wd, async () => {
|
|
3805
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3806
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3807
|
+
const hooksBefore = Buffer.from('{"hooks": {}}\n', "utf-8");
|
|
3808
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
3809
|
+
await writeFile(hooksPath, hooksBefore);
|
|
3810
|
+
await writeFile(configPath, configBefore);
|
|
3811
|
+
await Promise.all([chmod(hooksPath, 0o640), chmod(configPath, 0o640)]);
|
|
3812
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3813
|
+
if (stage === "after_rename" && target.kind === "hooks") {
|
|
3814
|
+
throw new Error("injected post-rename verification failure");
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
await assert.rejects(setup({
|
|
3818
|
+
scope: "user",
|
|
3819
|
+
installMode: "legacy",
|
|
3820
|
+
skipNativeAgentRefresh: true,
|
|
3821
|
+
codexFeaturesProbe: () => null,
|
|
3822
|
+
codexVersionProbe: () => null,
|
|
3823
|
+
}), /rolled back: injected post-rename verification failure/);
|
|
3824
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
3825
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3826
|
+
assert.equal((await stat(hooksPath)).mode & 0o777, 0o640);
|
|
3827
|
+
assert.equal((await stat(configPath)).mode & 0o777, 0o640);
|
|
3828
|
+
});
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
finally {
|
|
3832
|
+
resetFailureInjector?.();
|
|
3833
|
+
resetPlatform();
|
|
3834
|
+
await rm(wd, { recursive: true, force: true });
|
|
3835
|
+
}
|
|
3836
|
+
});
|
|
3837
|
+
it("preserves a concurrent replacement after staged source removal for manual recovery", async () => {
|
|
3838
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-post-remove-manual-recovery-"));
|
|
3839
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("linux");
|
|
3840
|
+
let resetFailureInjector;
|
|
3841
|
+
let resetTemporaryPath;
|
|
3842
|
+
try {
|
|
3843
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3844
|
+
await withTempCwd(wd, async () => {
|
|
3845
|
+
await setup({
|
|
3846
|
+
scope: "user",
|
|
3847
|
+
installMode: "legacy",
|
|
3848
|
+
skipNativeAgentRefresh: true,
|
|
3849
|
+
});
|
|
3850
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3851
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3852
|
+
const stagedDeletionPath = join(codexHomeDir, ".hooks-delete-post-remove");
|
|
3853
|
+
const hooksBefore = await readFile(hooksPath);
|
|
3854
|
+
const configBefore = await readFile(configPath);
|
|
3855
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
3856
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
|
|
3857
|
+
? stagedDeletionPath
|
|
3858
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-fallback`));
|
|
3859
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3860
|
+
if (stage === "after_remove" && target.kind === "hooks") {
|
|
3861
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3862
|
+
throw new Error("injected post-remove verification failure");
|
|
3863
|
+
}
|
|
3864
|
+
});
|
|
3865
|
+
await assert.rejects(setup({
|
|
3866
|
+
scope: "user",
|
|
3867
|
+
installMode: "plugin",
|
|
3868
|
+
skipNativeAgentRefresh: true,
|
|
3869
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3870
|
+
codexFeaturesProbe: () => [
|
|
3871
|
+
"hooks stable true",
|
|
3872
|
+
"plugin_hooks experimental true",
|
|
3873
|
+
"",
|
|
3874
|
+
].join("\n"),
|
|
3875
|
+
}), (error) => {
|
|
3876
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3877
|
+
assert.match(message, /manual recovery/);
|
|
3878
|
+
assert.ok(message.includes(hooksPath));
|
|
3879
|
+
return true;
|
|
3880
|
+
});
|
|
3881
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3882
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3883
|
+
assert.deepEqual(await readFile(stagedDeletionPath), hooksBefore);
|
|
3884
|
+
});
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
finally {
|
|
3888
|
+
resetFailureInjector?.();
|
|
3889
|
+
resetTemporaryPath?.();
|
|
3890
|
+
resetPlatform();
|
|
3891
|
+
await rm(wd, { recursive: true, force: true });
|
|
3892
|
+
}
|
|
3893
|
+
});
|
|
3894
|
+
it("never removes a concurrent replacement while rolling back a newly created artifact", async () => {
|
|
3895
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-remove-"));
|
|
3896
|
+
let resetFailureInjector;
|
|
3897
|
+
try {
|
|
3898
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3899
|
+
await withTempCwd(wd, async () => {
|
|
3900
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3901
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3902
|
+
const configBefore = Buffer.from('notify = ["node", "/tmp/user-notify.js"]\n', "utf-8");
|
|
3903
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
3904
|
+
await writeFile(configPath, configBefore);
|
|
3905
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3906
|
+
if (stage === "before_temp_write" && target.kind === "config") {
|
|
3907
|
+
throw new Error("injected config failure");
|
|
3908
|
+
}
|
|
3909
|
+
if (stage === "before_rollback_remove" && target.kind === "hooks") {
|
|
3910
|
+
writeFileSync(hooksPath, foreignHooks);
|
|
3911
|
+
}
|
|
3912
|
+
});
|
|
3913
|
+
await assert.rejects(setup({
|
|
3914
|
+
scope: "user",
|
|
3915
|
+
installMode: "legacy",
|
|
3916
|
+
skipNativeAgentRefresh: true,
|
|
3917
|
+
}), /rollback failed/);
|
|
3918
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
3919
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
3920
|
+
});
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
finally {
|
|
3924
|
+
resetFailureInjector?.();
|
|
3925
|
+
await rm(wd, { recursive: true, force: true });
|
|
3926
|
+
}
|
|
3927
|
+
});
|
|
3928
|
+
it("does not roll back when the second staged-deletion copy drifts after the first cleanup", async () => {
|
|
3929
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-staged-deletion-cleanup-"));
|
|
3930
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
3931
|
+
let resetFailureInjector;
|
|
3932
|
+
let resetTemporaryPath;
|
|
3933
|
+
try {
|
|
3934
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
3935
|
+
await withTempCwd(wd, async () => {
|
|
3936
|
+
await setup({
|
|
3937
|
+
scope: "user",
|
|
3938
|
+
installMode: "legacy",
|
|
3939
|
+
skipNativeAgentRefresh: true,
|
|
3940
|
+
});
|
|
3941
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
3942
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
3943
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
3944
|
+
const stagedDeletionPath = (path, purpose) => join(dirname(path), `.${basename(path)}.cleanup-${purpose}`);
|
|
3945
|
+
const foreignStagedCopy = Buffer.from("foreign second staged deletion\n", "utf-8");
|
|
3946
|
+
let cleanupCount = 0;
|
|
3947
|
+
let driftedStagedPath;
|
|
3948
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest(stagedDeletionPath);
|
|
3949
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
3950
|
+
if (stage !== "before_staged_cleanup" ||
|
|
3951
|
+
(target.kind !== "shim" && target.kind !== "hooks")) {
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
cleanupCount += 1;
|
|
3955
|
+
if (cleanupCount === 2) {
|
|
3956
|
+
driftedStagedPath = stagedDeletionPath(target.path, "delete");
|
|
3957
|
+
writeFileSync(driftedStagedPath, foreignStagedCopy);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
await assert.rejects(setup({
|
|
3961
|
+
scope: "user",
|
|
3962
|
+
installMode: "plugin",
|
|
3963
|
+
skipNativeAgentRefresh: true,
|
|
3964
|
+
pluginAgentsMdPrompt: async () => false,
|
|
3965
|
+
}), /committed but staged deletion cleanup failed/);
|
|
3966
|
+
assert.equal(cleanupCount, 2);
|
|
3967
|
+
assert.ok(driftedStagedPath);
|
|
3968
|
+
assert.deepEqual(await readFile(driftedStagedPath), foreignStagedCopy);
|
|
3969
|
+
assert.equal(existsSync(shimPath), false);
|
|
3970
|
+
assert.equal(existsSync(hooksPath), false);
|
|
3971
|
+
assert.match(await readFile(configPath, "utf-8"), /^plugin_hooks = true$/m);
|
|
3972
|
+
});
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
finally {
|
|
3976
|
+
resetFailureInjector?.();
|
|
3977
|
+
resetTemporaryPath?.();
|
|
3978
|
+
resetPlatform();
|
|
3979
|
+
await rm(wd, { recursive: true, force: true });
|
|
3980
|
+
}
|
|
3981
|
+
});
|
|
3982
|
+
it("treats applied hook, shim, and config snapshots as CAS through commit finalization", async () => {
|
|
3983
|
+
const fixtures = [
|
|
3984
|
+
{
|
|
3985
|
+
name: "later commit",
|
|
3986
|
+
stage: "before_temp_write",
|
|
3987
|
+
target: "shim",
|
|
3988
|
+
drift: "hooks",
|
|
3989
|
+
committed: false,
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
name: "staged cleanup",
|
|
3993
|
+
stage: "before_staged_cleanup",
|
|
3994
|
+
target: "hooks",
|
|
3995
|
+
drift: "shim",
|
|
3996
|
+
committed: false,
|
|
3997
|
+
},
|
|
3998
|
+
{
|
|
3999
|
+
name: "finalization",
|
|
4000
|
+
stage: "after_staged_cleanup",
|
|
4001
|
+
target: "config",
|
|
4002
|
+
drift: "config",
|
|
4003
|
+
committed: true,
|
|
4004
|
+
},
|
|
4005
|
+
];
|
|
4006
|
+
for (const fixture of fixtures) {
|
|
4007
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-setup-applied-${fixture.name}-`));
|
|
4008
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
4009
|
+
let resetFailureInjector;
|
|
4010
|
+
try {
|
|
4011
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4012
|
+
await withTempCwd(wd, async () => {
|
|
4013
|
+
await setup({
|
|
4014
|
+
scope: "user",
|
|
4015
|
+
installMode: "legacy",
|
|
4016
|
+
skipNativeAgentRefresh: true,
|
|
4017
|
+
});
|
|
4018
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
4019
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4020
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4021
|
+
const before = {
|
|
4022
|
+
shim: await readFile(shimPath),
|
|
4023
|
+
hooks: await readFile(hooksPath),
|
|
4024
|
+
config: await readFile(configPath),
|
|
4025
|
+
};
|
|
4026
|
+
const paths = { shim: shimPath, hooks: hooksPath, config: configPath };
|
|
4027
|
+
const foreign = Buffer.from(`foreign applied ${fixture.drift} ${fixture.name}\n`, "utf-8");
|
|
4028
|
+
let injected = false;
|
|
4029
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4030
|
+
if (stage !== fixture.stage ||
|
|
4031
|
+
target.kind !== fixture.target ||
|
|
4032
|
+
injected) {
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
injected = true;
|
|
4036
|
+
writeFileSync(paths[fixture.drift], foreign);
|
|
4037
|
+
});
|
|
4038
|
+
await assert.rejects(setup({
|
|
4039
|
+
scope: "user",
|
|
4040
|
+
installMode: "plugin",
|
|
4041
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4042
|
+
skipNativeAgentRefresh: true,
|
|
4043
|
+
}), fixture.committed
|
|
4044
|
+
? /committed but staged deletion cleanup failed during finalization/
|
|
4045
|
+
: /rollback failed/);
|
|
4046
|
+
assert.equal(injected, true, fixture.name);
|
|
4047
|
+
if (fixture.name === "staged cleanup") {
|
|
4048
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4049
|
+
assert.deepEqual(await readFile(shimPath), foreign);
|
|
4050
|
+
assert.notDeepEqual(await readFile(configPath), before.config);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
if (fixture.committed) {
|
|
4054
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4055
|
+
assert.equal(existsSync(shimPath), false);
|
|
4056
|
+
assert.deepEqual(await readFile(configPath), foreign);
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
assert.deepEqual(await readFile(hooksPath), fixture.drift === "hooks" ? foreign : before.hooks);
|
|
4060
|
+
assert.deepEqual(await readFile(shimPath), before.shim);
|
|
4061
|
+
assert.deepEqual(await readFile(configPath), before.config);
|
|
4062
|
+
});
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
finally {
|
|
4066
|
+
resetFailureInjector?.();
|
|
4067
|
+
resetPlatform();
|
|
4068
|
+
await rm(wd, { recursive: true, force: true });
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
it("preserves a same-byte foreign replacement made immediately after a native-hook rename", async () => {
|
|
4073
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-immediate-post-rename-"));
|
|
4074
|
+
let resetFailureInjector;
|
|
4075
|
+
try {
|
|
4076
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4077
|
+
await withTempCwd(wd, async () => {
|
|
4078
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4079
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4080
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
4081
|
+
let replacement;
|
|
4082
|
+
let replacedInode;
|
|
4083
|
+
await writeFile(configPath, configBefore);
|
|
4084
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4085
|
+
if (stage !== "after_rename" || target.kind !== "hooks")
|
|
4086
|
+
return;
|
|
4087
|
+
replacement = readFileSync(hooksPath);
|
|
4088
|
+
const ownedInode = lstatSync(hooksPath).ino;
|
|
4089
|
+
const foreignPath = join(codexHomeDir, ".foreign-hooks-same-byte");
|
|
4090
|
+
writeFileSync(foreignPath, replacement);
|
|
4091
|
+
assert.notEqual(lstatSync(foreignPath).ino, ownedInode);
|
|
4092
|
+
rmSync(hooksPath);
|
|
4093
|
+
renameSync(foreignPath, hooksPath);
|
|
4094
|
+
replacedInode = lstatSync(hooksPath).ino;
|
|
4095
|
+
assert.notEqual(replacedInode, ownedInode);
|
|
4096
|
+
});
|
|
4097
|
+
await assert.rejects(setup({
|
|
4098
|
+
scope: "user",
|
|
4099
|
+
installMode: "legacy",
|
|
4100
|
+
skipNativeAgentRefresh: true,
|
|
4101
|
+
codexFeaturesProbe: () => null,
|
|
4102
|
+
codexVersionProbe: () => null,
|
|
4103
|
+
}), /rollback failed/);
|
|
4104
|
+
assert.ok(replacement);
|
|
4105
|
+
assert.ok(replacedInode);
|
|
4106
|
+
assert.deepEqual(await readFile(hooksPath), replacement);
|
|
4107
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4108
|
+
});
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
finally {
|
|
4112
|
+
resetFailureInjector?.();
|
|
4113
|
+
await rm(wd, { recursive: true, force: true });
|
|
4114
|
+
}
|
|
4115
|
+
});
|
|
4116
|
+
it("preflights every staged native-hook recovery copy before rollback", async () => {
|
|
4117
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-recovery-preflight-"));
|
|
4118
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
4119
|
+
let resetFailureInjector;
|
|
4120
|
+
let resetTemporaryPath;
|
|
4121
|
+
try {
|
|
4122
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4123
|
+
await withTempCwd(wd, async () => {
|
|
4124
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
4125
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4126
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
4127
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4128
|
+
const configBefore = await readFile(configPath);
|
|
4129
|
+
const stagedHooksPath = join(codexHomeDir, ".hooks-preflight-recovery");
|
|
4130
|
+
resetTemporaryPath = setNativeHookTransactionTemporaryPathForTest((path, purpose) => path === hooksPath && purpose === "delete"
|
|
4131
|
+
? stagedHooksPath
|
|
4132
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-preflight`));
|
|
4133
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4134
|
+
if (stage !== "before_readback" || target.kind !== "config")
|
|
4135
|
+
return;
|
|
4136
|
+
rmSync(stagedHooksPath);
|
|
4137
|
+
throw new Error("injected missing staged recovery copy");
|
|
4138
|
+
});
|
|
4139
|
+
await assert.rejects(setup({
|
|
4140
|
+
scope: "user",
|
|
4141
|
+
installMode: "plugin",
|
|
4142
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4143
|
+
skipNativeAgentRefresh: true,
|
|
4144
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
4145
|
+
}), /rollback failed.*recovery preflight/);
|
|
4146
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4147
|
+
assert.equal(existsSync(shimPath), false);
|
|
4148
|
+
assert.notDeepEqual(await readFile(configPath), configBefore);
|
|
4149
|
+
assert.equal(existsSync(stagedHooksPath), false);
|
|
4150
|
+
});
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
finally {
|
|
4154
|
+
resetFailureInjector?.();
|
|
4155
|
+
resetTemporaryPath?.();
|
|
4156
|
+
resetPlatform();
|
|
4157
|
+
await rm(wd, { recursive: true, force: true });
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4160
|
+
it("stops later native-hook rollback restores when the first restored artifact drifts", async () => {
|
|
4161
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-rollback-restored-drift-"));
|
|
4162
|
+
const resetPlatform = setNativeHookTransactionPlatformForTest("win32");
|
|
4163
|
+
let resetFailureInjector;
|
|
4164
|
+
try {
|
|
4165
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4166
|
+
await withTempCwd(wd, async () => {
|
|
4167
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
4168
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4169
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
4170
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4171
|
+
const configBefore = await readFile(configPath);
|
|
4172
|
+
let injectedFailure = false;
|
|
4173
|
+
let drifted = false;
|
|
4174
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4175
|
+
if (stage === "before_readback" && target.kind === "config" && !injectedFailure) {
|
|
4176
|
+
injectedFailure = true;
|
|
4177
|
+
throw new Error("injected rollback start failure");
|
|
4178
|
+
}
|
|
4179
|
+
if (stage === "before_rollback_rename" && target.kind === "shim" && !drifted) {
|
|
4180
|
+
const restoredInode = lstatSync(configPath).ino;
|
|
4181
|
+
const foreignPath = join(codexHomeDir, ".foreign-restored-config");
|
|
4182
|
+
writeFileSync(foreignPath, configBefore);
|
|
4183
|
+
assert.notEqual(lstatSync(foreignPath).ino, restoredInode);
|
|
4184
|
+
rmSync(configPath);
|
|
4185
|
+
renameSync(foreignPath, configPath);
|
|
4186
|
+
drifted = lstatSync(configPath).ino !== restoredInode;
|
|
4187
|
+
}
|
|
4188
|
+
});
|
|
4189
|
+
await assert.rejects(setup({
|
|
4190
|
+
scope: "user",
|
|
4191
|
+
installMode: "plugin",
|
|
4192
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4193
|
+
skipNativeAgentRefresh: true,
|
|
4194
|
+
codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\n",
|
|
4195
|
+
}), /rollback failed/);
|
|
4196
|
+
assert.equal(injectedFailure, true);
|
|
4197
|
+
assert.equal(drifted, true);
|
|
4198
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4199
|
+
assert.equal(existsSync(shimPath), false);
|
|
4200
|
+
assert.equal(existsSync(hooksPath), false);
|
|
4201
|
+
});
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
finally {
|
|
4205
|
+
resetFailureInjector?.();
|
|
4206
|
+
resetPlatform();
|
|
4207
|
+
await rm(wd, { recursive: true, force: true });
|
|
4208
|
+
}
|
|
4209
|
+
});
|
|
4210
|
+
it("preserves a foreign inode injected after final setup rename validation", async () => {
|
|
4211
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-rename-claim-"));
|
|
4212
|
+
let resetFailureInjector;
|
|
4213
|
+
try {
|
|
4214
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4215
|
+
await withTempCwd(wd, async () => {
|
|
4216
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4217
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4218
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
4219
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
4220
|
+
await writeFile(configPath, configBefore);
|
|
4221
|
+
await writeFile(hooksPath, '{"hooks": {}}\n');
|
|
4222
|
+
let foreignInode;
|
|
4223
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4224
|
+
if (stage !== "after_final_rename_validation" || target.kind !== "hooks")
|
|
4225
|
+
return;
|
|
4226
|
+
const foreignPath = join(codexHomeDir, ".foreign-final-rename-hooks");
|
|
4227
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
4228
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
4229
|
+
rmSync(hooksPath);
|
|
4230
|
+
renameSync(foreignPath, hooksPath);
|
|
4231
|
+
});
|
|
4232
|
+
await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /precondition changed/);
|
|
4233
|
+
assert.ok(foreignInode);
|
|
4234
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
4235
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
4236
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4237
|
+
});
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
finally {
|
|
4241
|
+
resetFailureInjector?.();
|
|
4242
|
+
await rm(wd, { recursive: true, force: true });
|
|
4243
|
+
}
|
|
4244
|
+
});
|
|
4245
|
+
it("preserves a foreign inode injected after final setup removal validation", async () => {
|
|
4246
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-remove-claim-"));
|
|
4247
|
+
let resetFailureInjector;
|
|
4248
|
+
try {
|
|
4249
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4250
|
+
await withTempCwd(wd, async () => {
|
|
4251
|
+
await setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true });
|
|
4252
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4253
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4254
|
+
const configBefore = await readFile(configPath);
|
|
4255
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
4256
|
+
let foreignInode;
|
|
4257
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4258
|
+
if (stage !== "after_final_remove_validation" || target.kind !== "hooks")
|
|
4259
|
+
return;
|
|
4260
|
+
const foreignPath = join(codexHomeDir, ".foreign-final-remove-hooks");
|
|
4261
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
4262
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
4263
|
+
rmSync(hooksPath);
|
|
4264
|
+
renameSync(foreignPath, hooksPath);
|
|
4265
|
+
});
|
|
4266
|
+
await assert.rejects(setup({
|
|
4267
|
+
scope: "user",
|
|
4268
|
+
installMode: "plugin",
|
|
4269
|
+
pluginAgentsMdPrompt: async () => false,
|
|
4270
|
+
skipNativeAgentRefresh: true,
|
|
4271
|
+
}), /precondition changed/);
|
|
4272
|
+
assert.ok(foreignInode);
|
|
4273
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
4274
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
4275
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4276
|
+
});
|
|
4277
|
+
});
|
|
4278
|
+
}
|
|
4279
|
+
finally {
|
|
4280
|
+
resetFailureInjector?.();
|
|
4281
|
+
await rm(wd, { recursive: true, force: true });
|
|
4282
|
+
}
|
|
4283
|
+
});
|
|
4284
|
+
it("preserves a foreign inode injected after final setup rollback validation", async () => {
|
|
4285
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-final-restore-claim-"));
|
|
4286
|
+
let resetFailureInjector;
|
|
4287
|
+
try {
|
|
4288
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4289
|
+
await withTempCwd(wd, async () => {
|
|
4290
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4291
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4292
|
+
const configBefore = Buffer.from('model = "before"\n', "utf-8");
|
|
4293
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', "utf-8");
|
|
4294
|
+
await writeFile(configPath, configBefore);
|
|
4295
|
+
let foreignInode;
|
|
4296
|
+
resetFailureInjector = setNativeHookTransactionFailureInjectorForTest((stage, target) => {
|
|
4297
|
+
if (stage === "before_temp_write" && target.kind === "config") {
|
|
4298
|
+
throw new Error("injected rollback start");
|
|
4299
|
+
}
|
|
4300
|
+
if (stage !== "after_final_restore_validation" || target.kind !== "hooks")
|
|
4301
|
+
return;
|
|
4302
|
+
const foreignPath = join(codexHomeDir, ".foreign-final-restore-hooks");
|
|
4303
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
4304
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
4305
|
+
rmSync(hooksPath);
|
|
4306
|
+
renameSync(foreignPath, hooksPath);
|
|
4307
|
+
});
|
|
4308
|
+
await assert.rejects(setup({ scope: "user", installMode: "legacy", skipNativeAgentRefresh: true }), /rollback failed/);
|
|
4309
|
+
assert.ok(foreignInode);
|
|
4310
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
4311
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
4312
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4313
|
+
});
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
finally {
|
|
4317
|
+
resetFailureInjector?.();
|
|
4318
|
+
await rm(wd, { recursive: true, force: true });
|
|
4319
|
+
}
|
|
4320
|
+
});
|
|
4321
|
+
});
|
|
4322
|
+
describe("late setup failure transaction boundary", () => {
|
|
4323
|
+
it("does not commit config or hooks when a later setup phase fails", async () => {
|
|
4324
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-late-failure-"));
|
|
4325
|
+
let resetLateFailure;
|
|
4326
|
+
try {
|
|
4327
|
+
await withIsolatedUserHome(wd, async (codexHomeDir) => {
|
|
4328
|
+
await withTempCwd(wd, async () => {
|
|
4329
|
+
const hooksPath = join(codexHomeDir, "hooks.json");
|
|
4330
|
+
const configPath = join(codexHomeDir, "config.toml");
|
|
4331
|
+
const hooksBefore = Buffer.from('{"hooks":{"FutureEvent":[{"hooks":[{"type":"prompt","prompt":"keep"}]}]}}\n');
|
|
4332
|
+
const configBefore = Buffer.from('[features]\nhooks = true\n');
|
|
4333
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
4334
|
+
await writeFile(hooksPath, hooksBefore);
|
|
4335
|
+
await writeFile(configPath, configBefore);
|
|
4336
|
+
resetLateFailure = setSetupLatePhaseFailureInjectorForTest(() => {
|
|
4337
|
+
throw new Error("injected late setup phase failure");
|
|
4338
|
+
});
|
|
4339
|
+
await assert.rejects(setup({
|
|
4340
|
+
scope: "user",
|
|
4341
|
+
installMode: "legacy",
|
|
4342
|
+
skipNativeAgentRefresh: true,
|
|
4343
|
+
codexFeaturesProbe: () => null,
|
|
4344
|
+
codexVersionProbe: () => null,
|
|
4345
|
+
}), /injected late setup phase failure/);
|
|
4346
|
+
assert.deepEqual(await readFile(hooksPath), hooksBefore);
|
|
4347
|
+
assert.deepEqual(await readFile(configPath), configBefore);
|
|
4348
|
+
});
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
finally {
|
|
4352
|
+
resetLateFailure?.();
|
|
4353
|
+
await rm(wd, { recursive: true, force: true });
|
|
4354
|
+
}
|
|
4355
|
+
});
|
|
4356
|
+
});
|
|
4357
|
+
describe("persisted merge policy lifecycle", () => {
|
|
4358
|
+
it("preserves matching policy through review and lets explicit sets override reset or a scope change", async () => {
|
|
4359
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-lifecycle-"));
|
|
4360
|
+
try {
|
|
4361
|
+
await withIsolatedUserHome(wd, async () => {
|
|
4362
|
+
await withTempCwd(wd, async () => {
|
|
4363
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
4364
|
+
const statePath = join(wd, ".omx", "setup-scope.json");
|
|
4365
|
+
await writeFile(statePath, JSON.stringify({ scope: "user", installMode: "legacy", mergeAgents: true }));
|
|
4366
|
+
await setup({ persistedSetupReviewPrompt: async () => "review", setupScopePrompt: async () => "user", installModePrompt: async () => "legacy" });
|
|
4367
|
+
assert.equal(JSON.parse(await readFile(statePath, "utf-8")).mergeAgents, true);
|
|
4368
|
+
await setup({ persistedSetupReviewPrompt: async () => "reset", scope: "user", mergeAgents: false });
|
|
4369
|
+
assert.equal(JSON.parse(await readFile(statePath, "utf-8")).mergeAgents, false);
|
|
4370
|
+
await setup({ persistedSetupReviewPrompt: async () => "review", scope: "project", installMode: "legacy", mergeAgents: true });
|
|
4371
|
+
const changedScope = JSON.parse(await readFile(statePath, "utf-8"));
|
|
4372
|
+
assert.equal(changedScope.scope, "project");
|
|
4373
|
+
assert.equal(changedScope.mergeAgents, true);
|
|
4374
|
+
});
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
finally {
|
|
4378
|
+
await rm(wd, { recursive: true, force: true });
|
|
4379
|
+
}
|
|
4380
|
+
});
|
|
4381
|
+
it("removes an explicit policy when clear succeeds", async () => {
|
|
4382
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-clear-"));
|
|
4383
|
+
try {
|
|
4384
|
+
await withIsolatedUserHome(wd, async () => {
|
|
4385
|
+
await withTempCwd(wd, async () => {
|
|
4386
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
4387
|
+
const statePath = join(wd, ".omx", "setup-scope.json");
|
|
4388
|
+
await writeFile(statePath, JSON.stringify({ scope: "project", mergeAgents: true }));
|
|
4389
|
+
await setup({
|
|
4390
|
+
scope: "project",
|
|
4391
|
+
installMode: "legacy",
|
|
4392
|
+
mergeAgentsPolicy: { kind: "clear" },
|
|
4393
|
+
});
|
|
4394
|
+
assert.equal(Object.hasOwn(JSON.parse(await readFile(statePath, "utf-8")), "mergeAgents"), false);
|
|
4395
|
+
});
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
finally {
|
|
4399
|
+
await rm(wd, { recursive: true, force: true });
|
|
4400
|
+
}
|
|
4401
|
+
});
|
|
4402
|
+
it("covers persisted and explicit merge policy precedence across force", async () => {
|
|
4403
|
+
const wd = await mkdtemp(join(tmpdir(), "omx-setup-merge-policy-force-"));
|
|
4404
|
+
try {
|
|
4405
|
+
await withIsolatedUserHome(wd, async () => {
|
|
4406
|
+
await withTempCwd(wd, async () => {
|
|
4407
|
+
const statePath = join(wd, ".omx", "setup-scope.json");
|
|
4408
|
+
const agentsPath = join(wd, "AGENTS.md");
|
|
4409
|
+
const rows = [
|
|
4410
|
+
{ name: "persisted true + force", stored: true, policy: undefined, keepsCustom: true, persisted: true, refresh: "--merge-agents" },
|
|
4411
|
+
{ name: "persisted false + force", stored: false, policy: undefined, keepsCustom: false, persisted: false, refresh: "--no-merge-agents" },
|
|
4412
|
+
{ name: "absent + force", stored: undefined, policy: undefined, keepsCustom: false, persisted: undefined, refresh: undefined },
|
|
4413
|
+
{ name: "force + explicit true", stored: false, policy: { kind: "set", value: true }, keepsCustom: true, persisted: true, refresh: "--merge-agents" },
|
|
4414
|
+
{ name: "force + explicit false", stored: true, policy: { kind: "set", value: false }, keepsCustom: false, persisted: false, refresh: "--no-merge-agents" },
|
|
4415
|
+
{ name: "force + explicit clear", stored: true, policy: { kind: "clear" }, keepsCustom: false, persisted: undefined, refresh: undefined },
|
|
4416
|
+
];
|
|
4417
|
+
for (const row of rows) {
|
|
4418
|
+
await rm(join(wd, ".omx"), { recursive: true, force: true });
|
|
4419
|
+
await rm(join(wd, ".codex"), { recursive: true, force: true });
|
|
4420
|
+
await mkdir(join(wd, ".omx"), { recursive: true });
|
|
4421
|
+
await writeFile(statePath, JSON.stringify({ scope: "project", ...(row.stored === undefined ? {} : { mergeAgents: row.stored }) }));
|
|
4422
|
+
await writeFile(agentsPath, `# custom ${row.name}\n`);
|
|
4423
|
+
await setup({
|
|
4424
|
+
scope: "project",
|
|
4425
|
+
installMode: "legacy",
|
|
4426
|
+
force: true,
|
|
4427
|
+
mergeAgentsPolicy: row.policy,
|
|
4428
|
+
});
|
|
4429
|
+
const agents = await readFile(agentsPath, "utf-8");
|
|
4430
|
+
assert.equal(agents.includes(`# custom ${row.name}`), row.keepsCustom, row.name);
|
|
4431
|
+
const persisted = JSON.parse(await readFile(statePath, "utf-8"));
|
|
4432
|
+
assert.equal(persisted.mergeAgents, row.persisted, row.name);
|
|
4433
|
+
assert.equal(Object.hasOwn(persisted, "force"), false, row.name);
|
|
4434
|
+
const refreshArgs = resolveSetupRefreshArgs(wd);
|
|
4435
|
+
assert.equal(refreshArgs.includes("--force"), false, row.name);
|
|
4436
|
+
if (row.refresh)
|
|
4437
|
+
assert.equal(refreshArgs.includes(row.refresh), true, row.name);
|
|
4438
|
+
else
|
|
4439
|
+
assert.equal(refreshArgs.some((arg) => arg === "--merge-agents" || arg === "--no-merge-agents"), false, row.name);
|
|
4440
|
+
}
|
|
4441
|
+
});
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
finally {
|
|
4445
|
+
await rm(wd, { recursive: true, force: true });
|
|
4446
|
+
}
|
|
4447
|
+
});
|
|
2201
4448
|
});
|
|
2202
4449
|
//# sourceMappingURL=setup-install-mode.test.js.map
|