File size: 2,463 Bytes
d197cf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Launchd plist parser tests cover label extraction shared by lifecycle and diagnostics.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
  buildLaunchAgentPlist,
  parseLaunchdPlistLabel,
  readLaunchAgentProgramArgumentsFromFile,
} from "./launchd-plist.js";
import { decodeLaunchAgentPlistFixture } from "./launchd-plist.test-support.js";

vi.mock("../process/exec.js", async (importOriginal) => ({
  ...(await importOriginal<typeof import("../process/exec.js")>()),
  runExec: vi.fn(
    async (_command: string, _args: string[], options: { input: string | Uint8Array }) =>
      decodeLaunchAgentPlistFixture(options.input),
  ),
}));

describe("LaunchAgent environment round-trip", () => {
  it.each(["", "--max-old-space-size=24576"])(
    "preserves explicit NODE_OPTIONS=%j while omitting other empty values",
    async (nodeOptions) => {
      const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plist-env-"));
      const plistPath = path.join(dir, "gateway.plist");
      const programArguments = ["/usr/bin/node", "--max-old-space-size=16384", "gateway.js"];
      try {
        await fs.writeFile(
          plistPath,
          buildLaunchAgentPlist({
            label: "ai.openclaw.gateway",
            programArguments,
            stdoutPath: path.join(dir, "stdout.log"),
            stderrPath: path.join(dir, "stderr.log"),
            environment: { NODE_OPTIONS: nodeOptions, UNUSED: "", MISSING: undefined },
          }),
        );
        const command = await readLaunchAgentProgramArgumentsFromFile(plistPath);
        expect(command?.environment).toEqual({ NODE_OPTIONS: nodeOptions });
        expect(command?.programArguments).toEqual(programArguments);
      } finally {
        await fs.rm(dir, { recursive: true, force: true });
      }
    },
  );
});

describe("parseLaunchdPlistLabel", () => {
  it("decodes the XML entities accepted in launchd labels", () => {
    expect(
      parseLaunchdPlistLabel(
        "<plist><dict><key>Label</key><string>ai.openclaw.a&amp;b</string></dict></plist>",
      ),
    ).toBe("ai.openclaw.a&b");
  });

  it("returns null for missing or empty labels", () => {
    expect(parseLaunchdPlistLabel("<plist><dict/></plist>")).toBeNull();
    expect(
      parseLaunchdPlistLabel("<plist><dict><key>Label</key><string>  </string></dict></plist>"),
    ).toBeNull();
  });
});