fix: refine daemon launchd prep changes

This commit is contained in:
Gustavo Madeira Santana
2026-02-26 02:21:24 -05:00
parent cc630aa281
commit 7d72da289f
3 changed files with 42 additions and 14 deletions

View File

@@ -102,20 +102,6 @@
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one.
- Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available.
### Local full-CI one-shot record (2026-02-25)
- Ran local parity sweep for GitHub CI workflows (`workflow-sanity.yml`, `ci.yml`, `install-smoke.yml`, `sandbox-common-smoke.yml`) via `/tmp/run-openclaw-local-ci.sh`.
- Artifacts: `/tmp/openclaw-local-ci-20260225-221535.summary`, `/tmp/openclaw-local-ci-20260225-221535.log`.
- Result: `PASS=15`, `FAIL=15`, `SKIP=1` (`ci:secrets-zizmor` skipped because no workflow-file diff vs `origin/main`).
- `ci.yml` jobs with `if: false` (`deadcode`, `ios`) are disabled in GitHub CI and were not executed.
- Main local blockers seen in this run:
- `ci:check`: formatting failure in `src/daemon/launchd.ts`.
- `ci:checks-node-test` and `ci:macos-ts-tests`: test failures plus Node OOM during full `pnpm test`.
- `ci:skills-python-*` / `ci:secrets-*`: Homebrew Python PEP668 (`externally-managed-environment`) and missing `pre-commit`/`detect-secrets`.
- `ci:macos-swift*`: missing `swiftlint`/`swiftformat` and Swift `6.2.0` required vs local `6.1.0`.
- `ci:android-*`: Android SDK not configured (`ANDROID_HOME` / `apps/android/local.properties`).
- `sandbox-common-smoke:common-image`: passed after running the workflow command directly (temp script had unescaped `$u` with `set -u`).
## Commit & Pull Request Guidelines
**Full maintainer PR workflow (optional):** If you want the repo's end-to-end maintainer workflow (triage order, quality bar, rebase rules, commit/changelog conventions, co-contributor policy, and the `review-pr` > `prepare-pr` > `merge-pr` pipeline), see `.agents/skills/PR_WORKFLOW.md`. Maintainers may use other workflows; when a maintainer specifies a workflow, follow that. If no workflow is specified, default to PR_WORKFLOW.

View File

@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Daemon/macOS launchd: forward proxy env vars into supervised service environments, switch LaunchAgent keepalive policy to crash-only with throttling, and harden restart sequencing to `print -> bootout -> wait old pid exit -> bootstrap -> kickstart`. (#27276) thanks @frankekn.
- Android/Node invoke: remove native gateway WebSocket `Origin` header to avoid false origin rejections, unify invoke command registry/policy/error parsing paths, and keep command availability checks centralized to reduce dispatcher/advertisement drift. (#27257) Thanks @obviyus.
- CI/Windows: shard the Windows `checks-windows` test lane into two matrix jobs and honor explicit shard index overrides in `scripts/test-parallel.mjs` to reduce CI critical-path wall time. (#27234) Thanks @joshavant.

View File

@@ -12,6 +12,7 @@ import {
const state = vi.hoisted(() => ({
launchctlCalls: [] as string[][],
listOutput: "",
printOutput: "",
bootstrapError: "",
dirs: new Set<string>(),
files: new Map<string, string>(),
@@ -36,6 +37,9 @@ vi.mock("./exec-file.js", () => ({
if (call[0] === "list") {
return { stdout: state.listOutput, stderr: "", code: 0 };
}
if (call[0] === "print") {
return { stdout: state.printOutput, stderr: "", code: 0 };
}
if (call[0] === "bootstrap" && state.bootstrapError) {
return { stdout: "", stderr: state.bootstrapError, code: 1 };
}
@@ -72,6 +76,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
beforeEach(() => {
state.launchctlCalls.length = 0;
state.listOutput = "";
state.printOutput = "";
state.bootstrapError = "";
state.dirs.clear();
state.files.clear();
@@ -224,6 +229,42 @@ describe("launchd install", () => {
expect(bootstrapIndex).toBeLessThan(kickstartIndex);
});
it("waits for previous launchd pid to exit before bootstrapping", async () => {
const env = createDefaultLaunchdEnv();
state.printOutput = ["state = running", "pid = 4242"].join("\n");
const killSpy = vi.spyOn(process, "kill");
killSpy
.mockImplementationOnce(() => true)
.mockImplementationOnce(() => {
const err = new Error("no such process") as NodeJS.ErrnoException;
err.code = "ESRCH";
throw err;
});
vi.useFakeTimers();
try {
const restartPromise = restartLaunchAgent({
env,
stdout: new PassThrough(),
});
await vi.advanceTimersByTimeAsync(250);
await restartPromise;
expect(killSpy).toHaveBeenCalledWith(4242, 0);
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
const label = "ai.openclaw.gateway";
const bootoutIndex = state.launchctlCalls.findIndex(
(c) => c[0] === "bootout" && c[1] === `${domain}/${label}`,
);
const bootstrapIndex = state.launchctlCalls.findIndex((c) => c[0] === "bootstrap");
expect(bootoutIndex).toBeGreaterThanOrEqual(0);
expect(bootstrapIndex).toBeGreaterThanOrEqual(0);
expect(bootoutIndex).toBeLessThan(bootstrapIndex);
} finally {
vi.useRealTimers();
killSpy.mockRestore();
}
});
it("shows actionable guidance when launchctl gui domain does not support bootstrap", async () => {
state.bootstrapError = "Bootstrap failed: 125: Domain does not support specified action";
const env = createDefaultLaunchdEnv();