Skip to content

fix(service): complete the repair migration — docs, startup health, doctor, and update advice - #991

Merged
lidge-jun merged 6 commits into
devfrom
codex/service-repair-docs
Aug 4, 2026
Merged

fix(service): complete the repair migration — docs, startup health, doctor, and update advice#991
lidge-jun merged 6 commits into
devfrom
codex/service-repair-docs

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Follow-up to #980, produced by a six-round adversarial audit of the merged dev state (7343f0ba5).

What was wrong

ocx service repair shipped in #980 and is what the post-update refresh now runs — but the advice surfaces were never migrated with it. Seven places still told users to run ocx service install for a service that was already registered.

That is not cosmetic. repair refreshes the installed backend in place; install re-registers, which means a UAC prompt on Windows (schtasks /create) and a transactional backend switch that can replace a WinSW install with Task Scheduler.

Surface Was Now
docs-site CLI reference, 5 locales repair undocumented; EN example read Repair: ocx service install documented in heading, table, examples
docs-site Windows-memory guide, 5 locales re-bake via service install service repair
service.ts:2478 launchd hint install repair (its sibling at :1676 was fixed in #980; this one was missed)
autostart-health.ts stale/disabled/stopped service → install repair
startup-health-cache.ts ×2 stale-cache flipped repairinstall; DTO rebuild dropped the field both fixed
cli/index.ts printed install while the summary 3 lines later said repair state-derived
update/job.ts ran repair, then on failure said "run install as administrator" names the command that failed
doctor.ts, service.ts auth preflight, installLaunchd install state-derived

Two of these were runtime bugs I introduced mid-fix

StartupHealth.commands never declared repairService, so TypeScript could not catch either site:

markStartupHealthDiagnosticStale:  "ocx service repair"  ->  "ocx service install"
DTO rebuild:                        commands.repairService dropped entirely

The second meant that after a warm probe the dashboard recovery row rendered an empty command and would copy undefined. Declaring the field on the interface is what makes both sites type-checked rather than convention-checked.

Consistency

All six decision sites now use the identical predicate — serviceInstalled && !serviceConflict — because repairService() refuses a two-manager conflict outright, and install is the valid conflict recovery since installWindows removes the native backend first.

Still install, correctly: absent service, missing systemd unit, first-time setup, --native backend switch, and rollback.

Verification

  • bun x tsc --noEmit clean · bun run lint:gui exit 0 · bun run privacy:scan passed
  • Full suite: 8167 pass / 8 skip / 0 fail across 529 files
  • GUI: 583 pass / 0 fail
  • Every new guard red-green ablated

Three of my own tests were caught as vacuous during this audit and rewritten: a Windows guard that ran on macOS so the branch never executed, a stale-service test that asserted status but never the resulting command, and a preflight test that re-implemented the predicate as a lambda instead of calling it. The last one would have stayed green with the fix reverted.

Residual risk: no destructive live service-lifecycle test on a real Windows or macOS host; platform-manager behavior is covered by seams and focused tests.

…hd hint

Follow-up to #980, found by an adversarial audit of the merged dev state.

`ocx service repair` ships and is what the update path now runs, but the CLI
reference never documented it: the subcommand heading, the table and the example
block all listed install/start/stop/status/uninstall only, across all five
locales. Worse, the English status example literally read `Repair: ocx service
install`.

That advice is wrong in a way that costs the user something. `repair` refreshes
the installed backend in place; plain `install` re-registers, which needs
elevation on Windows and can switch a WinSW install to Task Scheduler
(src/service.ts:519, :1760). A user hand-recovering a service after an update
would hit a UAC prompt and possibly a backend switch, both avoidable.

src/service.ts:2478 had the same stale text in the launchd older-plist hint —
the sibling at :1676 was updated in #980 and this one was missed. Both are
installed-service recovery paths, so both say repair now.

First-install, absent-service and backend-switch messages still say install,
which is correct: repair refuses a service that is not installed.
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds ocx service repair to startup recovery, GUI command selection, diagnostics, update errors, tests, and lifecycle documentation. Installed non-conflicting services use repair. Absent or conflicting services use install.

Changes

Service repair integration

Layer / File(s) Summary
Define service recovery commands
src/codex/autostart-health.ts, gui/src/pages/startup-shared.ts
Startup health data now exposes repairService. Recovery selects repair for installed, non-conflicting services and install for absent or conflicting services.
Apply recovery selection in the GUI
gui/src/pages/startup-sections.tsx
The startup recovery section displays and copies the selected repair or install command.
Validate command selection
tests/autostart-health.test.ts
Tests cover installed, absent, and conflicting service states. The management DTO test includes ocx service repair.
Refine diagnostic and update guidance
src/cli/doctor.ts, src/update/job.ts, tests/doctor.test.ts
Doctor distinguishes installed-but-broken services from absent or conflicting services. Update errors recommend ocx service repair without platform-specific elevation instructions. Tests cover the diagnostic branches.
Document the repair command
docs-site/src/content/docs/*/reference/cli/lifecycle.md, docs-site/src/content/docs/*/troubleshooting/windows-memory.md, src/service.ts
Documentation adds repair to lifecycle syntax, command tables, examples, stale-service guidance, and Windows runtime override instructions. The service status report recommends repair for stale launchd definitions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StartupHealth
  participant StartupRecoverySection
  participant User
  StartupHealth->>StartupHealth: select repair or install from service state
  StartupHealth-->>StartupRecoverySection: provide recovery command
  User->>StartupRecoverySection: view or copy command
  StartupRecoverySection-->>User: display or copy selected command
Loading

Possibly related PRs

Suggested labels: enhancement, documentation

Suggested reviewers: ingwannu, wibias

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the repair migration and identifies the affected documentation, startup health, doctor, and update guidance.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/service-repair-docs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba68e9037b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/service.ts
const staleLine = stalePlist && stalePlist.loaded && !stalePlist.matchesPlist
? " launchd is running an OLDER plist than the one on disk.\n"
+ ` Fix: launchctl bootout gui/$(id -u)/${LABEL} && ocx service install\n`
+ ` Fix: launchctl bootout gui/$(id -u)/${LABEL} && ocx service repair\n`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the stale-plist repair command in the service test

This changes user-facing recovery behavior, but the existing adds the bootout hint when launchd runs an older plist test in tests/service.test.ts checks only for OLDER plist and bootout, so it would still pass if this command regressed to ocx service install. Add a focused assertion for ocx service repair—ideally the complete bootout … && ocx service repair sequence—to preserve the corrected recovery path.

AGENTS.md reference: src/AGENTS.md:L22-L25

Useful? React with 👍 / 👎.

| none | Create/update and start the service. |
| `install` | Create and start the service. |
| `install` | Create and start the service. Registers it, which on Windows needs elevation. |
| `repair` | Refresh an installed service in place and restart it, without re-registering it. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the no-registration claim to Windows

The claim that repair does not re-register the service is only true for the Windows Task Scheduler path: repairService() delegates macOS repairs to installLaunchd(), which explicitly unloads and reloads the plist, and delegates Linux repairs to the normal install/reload path. As written, this newly added cross-platform table row misdescribes macOS behavior; qualify the no-registration guarantee as Windows-specific or describe the portable guarantee that the installed backend is preserved.

AGENTS.md reference: docs-site/AGENTS.md:L5-L10

Useful? React with 👍 / 👎.

## Background service

### `ocx service [install|start|stop|status|uninstall|remove]`
### `ocx service [install|repair|start|stop|status|uninstall|remove]`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add repair to the built-in service help

The reference now advertises repair, but src/cli/help.ts still renders ocx help service and ocx service --help as ocx service [install|start|stop|status|uninstall|remove]. This leaves the shipped CLI help contradicting the newly synchronized public reference and continues to hide the recovery command from users who consult local help; update the service help entry and its CLI-help coverage alongside this heading.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

…tration

Second instance of the same defect the docs commit fixed, found while checking
whether that fix was complete.

startupHealthSummary() renders 'background service files are stale' and
'installed service is disabled, stopped, or unhealthy' — both cases where a
service is already REGISTERED — and then told the user to run
'ocx service install'. The dashboard recovery panel hardcoded the same command
in its copy button.

install re-registers: it needs elevation on Windows (schtasks /create) and its
transactional backend switch can replace a WinSW install with Task Scheduler.
So the advice handed a UAC prompt and a possible backend change to someone whose
only problem was stale assets. repair rewrites those assets and restarts the
existing registration in place.

recommendedCommand now picks repair when serviceInstalled and not conflicting;
a genuinely absent service still gets install, and a conflict still gets install
because repairService() refuses a two-manager conflict outright and the user must
uninstall first. The GUI derives its command the same way instead of hardcoding.

Red-green: restoring the unconditional installService fails the stale/disabled/
stopped guard. The DTO contract test was updated for the new commands entry, and
eslint caught the constant landing in the tray section rather than the recovery
section it renders in.

11 pass / 0 fail autostart-health, 583 pass / 0 fail gui.
…troubleshooting docs

Third round of the same audit. The reviewer re-checked my first fix and found
three more surfaces still advising re-registration for an installed service.

update/job.ts: the refresh it runs is 'service repair' (serviceReinstallArgs),
but the failure message told Windows users to run 'ocx service install' as
administrator. That is the exact post-update path #980 changed — advising
install there sends the user to a UAC prompt and a possible WinSW-to-scheduler
switch to fix a service that is already registered. It now names the command
that actually failed, on every platform, so its output explains why.

doctor.ts proxyDownRestartHint(): took only serviceViable, which conflates 'no
service at all' with 'registered but stale or stopped'. Only the first wants
install. It now takes serviceInstalled/serviceConflict and points an installed
service at repair; a conflict still gets install because repairService() refuses
a two-manager conflict and the user must uninstall first.

docs-site troubleshooting/windows-memory.md in all five locales: the paragraph
explicitly discusses 'an already-installed service' and then said to re-run
'ocx service install' to re-bake OPENCODEX_BUN_PATH. repairService() rewrites
exactly those scheduler/WinSW assets in place, so repair is both correct and
cheaper.

The reviewer also flagged autostart-health, which bee1cc7 already fixed — it
audited the earlier commit. Its note that the existing stale-service test was
vacuous (asserting status but never the resulting command) was accurate, and the
guard added in that commit closes it.

196 pass / 0 fail across doctor, autostart-health, update-job, service, and
update-stop-first.
@lidge-jun lidge-jun changed the title docs(service): document the repair subcommand and fix the stale launchd hint fix(service): complete the repair migration — docs, startup health, doctor, and update advice Aug 4, 2026
@github-actions github-actions Bot added bug Something isn't working and removed documentation Improvements or additions to documentation labels Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gui/src/pages/startup-sections.tsx (1)

228-251: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the Repair action for every non-viable installed service.

Lines 232-234 select repairService for an installed, non-conflicting disabled or stopped service. However, Line 94 sets serviceNeedsRepair only when serviceStale is true. The details section then shows neither Install nor Repair for disabled or stopped services.

Align the direct action with the recovery command.

Proposed fix
-  const serviceNeedsRepair = data.serviceSupported && data.serviceInstalled && data.serviceStale && !data.serviceConflict;
+  const serviceNeedsRepair = data.serviceSupported
+    && data.serviceInstalled
+    && !data.serviceConflict
+    && (data.serviceStale || !data.serviceViable);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/pages/startup-sections.tsx` around lines 228 - 251, The condition
selecting repairService in the serviceCommand logic (checking
data.serviceInstalled && !data.serviceConflict) does not match the condition for
setting serviceNeedsRepair, which currently only checks serviceStale. Update the
condition that sets serviceNeedsRepair to align with the serviceCommand
selection logic so that disabled or stopped installed services without conflicts
are eligible for the Repair action in the details section, matching what the
recovery command offers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/codex/autostart-health.ts`:
- Around line 55-59: Add repairService: string to the source
StartupHealth.commands interface alongside installService, matching the existing
COMMANDS entry and returned field so typed consumers can access it.

---

Outside diff comments:
In `@gui/src/pages/startup-sections.tsx`:
- Around line 228-251: The condition selecting repairService in the
serviceCommand logic (checking data.serviceInstalled && !data.serviceConflict)
does not match the condition for setting serviceNeedsRepair, which currently
only checks serviceStale. Update the condition that sets serviceNeedsRepair to
align with the serviceCommand selection logic so that disabled or stopped
installed services without conflicts are eligible for the Repair action in the
details section, matching what the recovery command offers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4e1ad00e-960a-4894-b536-4e8a5aea937a

📥 Commits

Reviewing files that changed from the base of the PR and between ba68e90 and bee1cc7.

📒 Files selected for processing (4)
  • gui/src/pages/startup-sections.tsx
  • gui/src/pages/startup-shared.ts
  • src/codex/autostart-health.ts
  • tests/autostart-health.test.ts

Comment thread src/codex/autostart-health.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/update/job.ts`:
- Around line 826-836: Add focused regression coverage for restartAfterUpdate
when the service refresh returns a non-zero status: verify the installed
non-conflicting, conflicting, and direct-start fallback paths all produce the
expected failed-refresh guidance, including the manual “ocx service repair” and
status commands. Keep the existing proxyDownRestartHint tests unchanged and use
the existing service fixtures/helpers.
- Around line 826-836: Update the installed-service refresh failure path around
restartAfterUpdate so it distinguishes backend conflicts before choosing
recovery guidance. Propagate the conflict state or repair diagnostic from the
service refresh flow, directing conflicting installations to run `ocx service
uninstall` followed by `ocx service install`, while retaining `ocx service
repair` guidance for non-conflicting installed services.

In `@tests/doctor.test.ts`:
- Around line 541-546: Strengthen the assertions in the proxyDownRestartHint
test for both the absent-service and serviceConflict cases so the returned
guidance contains “ocx service install” but does not contain the repair command.
Update the assertions around proxyDownRestartHint and preserve the existing
positive install checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eee85ae1-a889-438f-9c7a-aa080f07a246

📥 Commits

Reviewing files that changed from the base of the PR and between bee1cc7 and 0662e34.

📒 Files selected for processing (8)
  • docs-site/src/content/docs/ja/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/ko/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/ru/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/troubleshooting/windows-memory.md
  • docs-site/src/content/docs/zh-cn/troubleshooting/windows-memory.md
  • src/cli/doctor.ts
  • src/update/job.ts
  • tests/doctor.test.ts

Comment thread src/update/job.ts
Comment on lines +826 to +836
// The refresh that just failed was `ocx service repair` (serviceReinstallArgs),
// which needs no elevation because it never calls `schtasks /create`. Advising
// `install` here would send the user to re-registration — a UAC prompt on
// Windows and a possible WinSW-to-scheduler backend switch — to fix a service
// that is already registered. Point at the same command that failed so its
// output explains why, on every platform.
updateJob(
job,
{},
`Service reinstall failed (exit ${result.status ?? "?"}); falling back to a direct proxy start.`
+ (process.platform === "win32"
? " Run 'ocx service install' as administrator to refresh the background service manager."
: " Run 'ocx service install' by hand to see the reason, then 'ocx service status'."),
`Service refresh failed (exit ${result.status ?? "?"}); falling back to a direct proxy start.`
+ " Run 'ocx service repair' by hand to see the reason, then 'ocx service status'.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a focused regression test for the failed refresh message.

The supplied test changes cover proxyDownRestartHint, but they do not exercise restartAfterUpdate when the service command returns a non-zero status. Add coverage for installed non-conflicting services, conflicting services, and the direct-start fallback.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn, spawnSync, type ChildProcess } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/update/job.ts` around lines 826 - 836, Add focused regression coverage
for restartAfterUpdate when the service refresh returns a non-zero status:
verify the installed non-conflicting, conflicting, and direct-start fallback
paths all produce the expected failed-refresh guidance, including the manual
“ocx service repair” and status commands. Keep the existing proxyDownRestartHint
tests unchanged and use the existing service fixtures/helpers.

Source: Path instructions


🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve the conflict-specific recovery command.

Line 826 always points users to ocx service repair after an installed-service refresh fails. src/service.ts Lines 1772-1777 reject repairService() when both backends are present and require ocx service uninstall followed by ocx service install. This branch does not inspect the conflict state, so a conflicting installation can receive a command that fails for the same reason again. Carry the conflict state into restartAfterUpdate, or propagate the repair diagnostic, so conflicts receive uninstall/install guidance and installed non-conflicting services receive repair guidance.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn, spawnSync, type ChildProcess } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/update/job.ts` around lines 826 - 836, Update the installed-service
refresh failure path around restartAfterUpdate so it distinguishes backend
conflicts before choosing recovery guidance. Propagate the conflict state or
repair diagnostic from the service refresh flow, directing conflicting
installations to run `ocx service uninstall` followed by `ocx service install`,
while retaining `ocx service repair` guidance for non-conflicting installed
services.

Comment thread tests/doctor.test.ts
Comment on lines +541 to +546
const absent = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: false });
expect(absent).toContain("ocx service install");

// A two-manager conflict must be uninstalled first; repairService() refuses it.
const conflict = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: true, serviceConflict: true });
expect(conflict).toContain("ocx service install");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that conflict guidance excludes repair.

Line 545 checks only for ocx service install. A regression that emits both install and repair would pass while violating the conflict rule. Add negative assertions for the conflict and absent cases.

Suggested assertions
     const absent = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: false });
     expect(absent).toContain("ocx service install");
+    expect(absent).not.toContain("ocx service repair");

     // A two-manager conflict must be uninstalled first; repairService() refuses it.
     const conflict = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: true, serviceConflict: true });
     expect(conflict).toContain("ocx service install");
+    expect(conflict).not.toContain("ocx service repair");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const absent = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: false });
expect(absent).toContain("ocx service install");
// A two-manager conflict must be uninstalled first; repairService() refuses it.
const conflict = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: true, serviceConflict: true });
expect(conflict).toContain("ocx service install");
const absent = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: false });
expect(absent).toContain("ocx service install");
expect(absent).not.toContain("ocx service repair");
// A two-manager conflict must be uninstalled first; repairService() refuses it.
const conflict = proxyDownRestartHint({ proxyRunning: false, port: 10100, serviceViable: false, serviceInstalled: true, serviceConflict: true });
expect(conflict).toContain("ocx service install");
expect(conflict).not.toContain("ocx service repair");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doctor.test.ts` around lines 541 - 546, Strengthen the assertions in
the proxyDownRestartHint test for both the absent-service and serviceConflict
cases so the returned guidance contains “ocx service install” but does not
contain the repair command. Update the assertions around proxyDownRestartHint
and preserve the existing positive install checks.

…remaining advice

Round 3 of the audit found two RUNTIME bugs I introduced in bee1cc7, plus the
last stale advice surfaces. Both runtime bugs were reproduced before fixing.

The DTO type never declared repairService, so TypeScript could not catch either:

  markStartupHealthDiagnosticStale() re-derived recommendedCommand and hardcoded
  installService, silently undoing the repair choice on every stale-cache read —
  the exact path the dashboard hits while a probe revalidates:
    before: ocx service repair -> after stale marking: ocx service install

  the subprocess DTO rebuild copied installService/installShim/restoreNative and
  dropped repairService, so after a warm probe the GUI recovery row rendered an
  empty command and would copy undefined.

StartupHealth.commands now declares repairService, which is what makes both
sites type-checked rather than convention-checked.

Remaining advice surfaces, all reachable with a service already installed:
- src/cli/index.ts printed 'install the persistent service' unconditionally
  while the service summary three lines later told the same user to repair —
  one status report, two contradictory instructions.
- assertServiceAuthEnvironment() and installLaunchd() are both reached THROUGH
  repair (macOS repair delegates straight to installLaunchd), so they now name
  the command the caller actually ran.
- structure/05 and an update/index comment described the old command set.
- Two GUI fixtures omitted repairService, which is what let an incomplete DTO
  look acceptable in those tests.

Red-green: restoring the unconditional installService in the stale-cache path
fails the new guard. The pre-existing stale-cache test asserted status and
protection but never the resulting command, which is why this regression was
invisible to it.

254 pass / 0 fail across service, startup, doctor, update and winsw suites;
583 pass / 0 fail gui.
…fuses

Round 4 blocker. My preflight retry hint checked diagnoseService().installed but
ignored conflict, unlike the other three sites.

Concrete failure: Windows with both Task Scheduler and WinSW registered,
non-loopback hostname, no OPENCODEX_API_AUTH_TOKEN. The preflight runs before
repairService() (service.ts:2544), so the user was told to set the token and
rerun 'ocx service repair' — a command repairService() then refuses outright
(:1783), which sends them to uninstall-then-install. Two round trips to reach
advice the first message could have given.

Install IS the valid recovery for a conflict: installWindows removes the native
backend before registering the scheduler one. So the predicate now matches the
other three sites exactly: installed && !conflict ? repair : install.

The message's first sentence also said the token is required 'before installing'
even when reached from repair; it now says refreshing or installing to match.

Also fixed while checking the reviewer's second question: installLaunchd()
captured its installed-signal AFTER writeFileSync(plistPath()), so existsSync
was unconditionally true and every fresh install would have been told to repair.
Captured before the write instead.
The round-5 reviewer caught that my guard was vacuous: it defined a local pick()
lambda duplicating the predicate and asserted against that, so reverting the
production fix to the broken installed-only logic would have left it green. A
guard that cannot fail is worse than no guard — it reads as coverage.

Extracted serviceRetryCommand() from assertServiceAuthEnvironment() and the test
now calls it with the three diagnostic shapes directly. It takes a
Pick<ServiceDiagnostic, 'installed' | 'conflict'> defaulting to diagnoseService(),
so production behavior is unchanged and the test needs no environment mocking.

Red-green: reverting the predicate to 'diag.installed ? repair : install' now
fails the guard (97 pass / 1 fail), and restoring it returns 98 pass / 0 fail.

This is the third vacuous or wrong-reason test caught in this session — the
Windows GUI guard that ran on macOS, the stale-service test that asserted status
but never the command, and now this one.
@lidge-jun
lidge-jun merged commit 6712887 into dev Aug 4, 2026
28 checks passed
@lidge-jun
lidge-jun deleted the codex/service-repair-docs branch August 4, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant