When installing the LaunchAgent/systemd service, the CLI was using fs.realpath() to resolve the entry.js path, which converted stable symlinked paths (e.g. node_modules/clawdbot) into version-specific paths (e.g. .pnpm/clawdbot@X.Y.Z/...). This caused the service to break after pnpm updates because the old versioned path no longer exists, even though the symlink still works. Now we prefer the original (symlinked) path when it's valid, keeping service configs stable across package version updates.
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const fsMocks = vi.hoisted(() => ({
|
|
access: vi.fn(),
|
|
realpath: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("node:fs/promises", () => ({
|
|
default: { access: fsMocks.access, realpath: fsMocks.realpath },
|
|
access: fsMocks.access,
|
|
realpath: fsMocks.realpath,
|
|
}));
|
|
|
|
import { resolveGatewayProgramArguments } from "./program-args.js";
|
|
|
|
const originalArgv = [...process.argv];
|
|
|
|
afterEach(() => {
|
|
process.argv = [...originalArgv];
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
describe("resolveGatewayProgramArguments", () => {
|
|
it("uses realpath-resolved dist entry when running via npx shim", async () => {
|
|
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot");
|
|
const entryPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js");
|
|
process.argv = ["node", argv1];
|
|
fsMocks.realpath.mockResolvedValue(entryPath);
|
|
fsMocks.access.mockImplementation(async (target: string) => {
|
|
if (target === entryPath) {
|
|
return;
|
|
}
|
|
throw new Error("missing");
|
|
});
|
|
|
|
const result = await resolveGatewayProgramArguments({ port: 18789 });
|
|
|
|
expect(result.programArguments).toEqual([
|
|
process.execPath,
|
|
entryPath,
|
|
"gateway",
|
|
"--port",
|
|
"18789",
|
|
]);
|
|
});
|
|
|
|
it("prefers symlinked path over realpath for stable service config", async () => {
|
|
// Simulates pnpm global install where node_modules/clawdbot is a symlink
|
|
// to .pnpm/clawdbot@X.Y.Z/node_modules/clawdbot
|
|
const symlinkPath = path.resolve(
|
|
"/Users/test/Library/pnpm/global/5/node_modules/clawdbot/dist/entry.js",
|
|
);
|
|
const realpathResolved = path.resolve(
|
|
"/Users/test/Library/pnpm/global/5/node_modules/.pnpm/clawdbot@2026.1.21-2/node_modules/clawdbot/dist/entry.js",
|
|
);
|
|
process.argv = ["node", symlinkPath];
|
|
fsMocks.realpath.mockResolvedValue(realpathResolved);
|
|
fsMocks.access.mockResolvedValue(undefined); // Both paths exist
|
|
|
|
const result = await resolveGatewayProgramArguments({ port: 18789 });
|
|
|
|
// Should use the symlinked path, not the realpath-resolved versioned path
|
|
expect(result.programArguments[1]).toBe(symlinkPath);
|
|
expect(result.programArguments[1]).not.toContain("@2026.1.21-2");
|
|
});
|
|
|
|
it("falls back to node_modules package dist when .bin path is not resolved", async () => {
|
|
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot");
|
|
const indexPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js");
|
|
process.argv = ["node", argv1];
|
|
fsMocks.realpath.mockRejectedValue(new Error("no realpath"));
|
|
fsMocks.access.mockImplementation(async (target: string) => {
|
|
if (target === indexPath) {
|
|
return;
|
|
}
|
|
throw new Error("missing");
|
|
});
|
|
|
|
const result = await resolveGatewayProgramArguments({ port: 18789 });
|
|
|
|
expect(result.programArguments).toEqual([
|
|
process.execPath,
|
|
indexPath,
|
|
"gateway",
|
|
"--port",
|
|
"18789",
|
|
]);
|
|
});
|
|
});
|