mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 11:34:16 +00:00
fix: macOS-only Parallels smoke harness regression (#75293)
* fix: macOS-only Parallels smoke harness regression * fix(parallels): keep macOS update smoke script owner-readable * fix: macOS-only Parallels smoke harness regression * test(parallels): exercise macOS guest script identity Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> --------- Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -99,6 +99,11 @@ interface SpawnLoggedOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
interface MacosUpdateExec {
|
||||
execArgs: string[];
|
||||
ownerUser: string;
|
||||
}
|
||||
|
||||
interface NpmUpdateSummary {
|
||||
packageSpec: string;
|
||||
updateTarget: string;
|
||||
@@ -1156,26 +1161,24 @@ export class NpmUpdateSmoke {
|
||||
timeoutMs: number,
|
||||
ctx: UpdateJobContext,
|
||||
): Promise<void> {
|
||||
const macosUpdateExec = this.resolveMacosUpdateExec(ctx);
|
||||
const scriptPath = this.writeGuestScript(
|
||||
this.macosVm,
|
||||
script,
|
||||
"openclaw-parallels-npm-update-macos",
|
||||
{ execArgs: macosUpdateExec.execArgs, mode: "700" },
|
||||
);
|
||||
const macosExecArgs = this.resolveMacosUpdateExecArgs(ctx);
|
||||
const sudoUserArgIndex = macosExecArgs.indexOf("-u");
|
||||
const sudoUser =
|
||||
sudoUserArgIndex >= 0 && sudoUserArgIndex + 1 < macosExecArgs.length
|
||||
? macosExecArgs[sudoUserArgIndex + 1]
|
||||
: "";
|
||||
if (sudoUser) {
|
||||
run("prlctl", ["exec", this.macosVm, "/usr/sbin/chown", sudoUser, scriptPath], {
|
||||
run(
|
||||
"prlctl",
|
||||
["exec", this.macosVm, "/usr/sbin/chown", macosUpdateExec.ownerUser, scriptPath],
|
||||
{
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
try {
|
||||
const status = await this.runStreamingToJobLog(
|
||||
"prlctl",
|
||||
["exec", this.macosVm, ...macosExecArgs, "/bin/bash", scriptPath],
|
||||
["exec", this.macosVm, ...macosUpdateExec.execArgs, "/bin/bash", scriptPath],
|
||||
timeoutMs,
|
||||
ctx,
|
||||
);
|
||||
@@ -1187,7 +1190,7 @@ export class NpmUpdateSmoke {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveMacosUpdateExecArgs(ctx: UpdateJobContext): string[] {
|
||||
private resolveMacosUpdateExec(ctx: UpdateJobContext): MacosUpdateExec {
|
||||
const guestPath =
|
||||
"/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/usr/local/bin:/usr/local/sbin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin";
|
||||
const currentUser = run("prlctl", ["exec", this.macosVm, "--current-user", "whoami"], {
|
||||
@@ -1197,7 +1200,10 @@ export class NpmUpdateSmoke {
|
||||
});
|
||||
const user = currentUser.stdout.trim().replaceAll("\r", "").split("\n").at(-1) ?? "";
|
||||
if (currentUser.status === 0 && /^[A-Za-z0-9._-]+$/.test(user)) {
|
||||
return ["--current-user", "/usr/bin/env", `PATH=${guestPath}`];
|
||||
return {
|
||||
execArgs: ["--current-user", "/usr/bin/env", `PATH=${guestPath}`],
|
||||
ownerUser: user,
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackUser = this.resolveMacosDesktopUser();
|
||||
@@ -1210,17 +1216,20 @@ export class NpmUpdateSmoke {
|
||||
`desktop user unavailable via Parallels --current-user; using root sudo fallback for ${fallbackUser}\n`,
|
||||
);
|
||||
const home = this.resolveMacosDesktopHome(fallbackUser);
|
||||
return [
|
||||
"/usr/bin/sudo",
|
||||
"-H",
|
||||
"-u",
|
||||
fallbackUser,
|
||||
"/usr/bin/env",
|
||||
`HOME=${home}`,
|
||||
`USER=${fallbackUser}`,
|
||||
`LOGNAME=${fallbackUser}`,
|
||||
`PATH=${guestPath}`,
|
||||
];
|
||||
return {
|
||||
execArgs: [
|
||||
"/usr/bin/sudo",
|
||||
"-H",
|
||||
"-u",
|
||||
fallbackUser,
|
||||
"/usr/bin/env",
|
||||
`HOME=${home}`,
|
||||
`USER=${fallbackUser}`,
|
||||
`LOGNAME=${fallbackUser}`,
|
||||
`PATH=${guestPath}`,
|
||||
],
|
||||
ownerUser: fallbackUser,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveMacosDesktopUser(): string {
|
||||
@@ -1321,9 +1330,16 @@ export class NpmUpdateSmoke {
|
||||
}
|
||||
}
|
||||
|
||||
private writeGuestScript(vm: string, script: string, prefix: string): string {
|
||||
private writeGuestScript(
|
||||
vm: string,
|
||||
script: string,
|
||||
prefix: string,
|
||||
options: { execArgs?: string[]; mode?: "700" | "755" } = {},
|
||||
): string {
|
||||
const execArgs = options.execArgs ?? [];
|
||||
const mode = options.mode ?? "755";
|
||||
const scriptPath = `/tmp/${prefix}-${randomUUID()}.sh`;
|
||||
const write = run("prlctl", ["exec", vm, "/usr/bin/tee", scriptPath], {
|
||||
const write = run("prlctl", ["exec", vm, ...execArgs, "/usr/bin/tee", scriptPath], {
|
||||
check: false,
|
||||
input: script,
|
||||
quiet: true,
|
||||
@@ -1333,7 +1349,7 @@ export class NpmUpdateSmoke {
|
||||
throw new Error(`failed to write guest script ${scriptPath}: ${write.stderr.trim()}`);
|
||||
}
|
||||
try {
|
||||
const chmod = run("prlctl", ["exec", vm, "/bin/chmod", "755", scriptPath], {
|
||||
const chmod = run("prlctl", ["exec", vm, ...execArgs, "/bin/chmod", mode, scriptPath], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: 30_000,
|
||||
|
||||
@@ -254,6 +254,88 @@ exit 1
|
||||
expect(log.match(/^cleanup$/gm)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("uses one macOS guest identity to write and execute update scripts", async () => {
|
||||
const root = makeTempDir();
|
||||
const logPath = path.join(root, "prlctl.log");
|
||||
const prlctlPath = path.join(root, "prlctl");
|
||||
writeFileSync(
|
||||
prlctlPath,
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
log_path=${JSON.stringify(logPath)}
|
||||
printf '%s\\n' "$*" >>"$log_path"
|
||||
args=" $* "
|
||||
if [[ "$args" == *" --current-user whoami "* ]]; then
|
||||
printf 'desktop-user\\n'
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$args" == *" /usr/bin/tee /tmp/openclaw-parallels-npm-update-macos-"* ]]; then
|
||||
cat >/dev/null
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$args" == *" /bin/chmod 700 /tmp/openclaw-parallels-npm-update-macos-"* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$args" == *" /usr/sbin/chown desktop-user /tmp/openclaw-parallels-npm-update-macos-"* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$args" == *" /bin/rm -f /tmp/openclaw-parallels-npm-update-macos-"* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
`,
|
||||
);
|
||||
chmodSync(prlctlPath, 0o755);
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENAI_API_KEY: "test-key",
|
||||
PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
async () => {
|
||||
const smoke = new NpmUpdateSmoke({
|
||||
...TEST_AUTH,
|
||||
dependencyTarballs: [],
|
||||
registryPackageTarballs: [],
|
||||
json: false,
|
||||
packageSpec: "openclaw@latest",
|
||||
platforms: new Set<Platform>(["macos"]),
|
||||
provider: "openai",
|
||||
updateTarget: "local-main",
|
||||
});
|
||||
const stream = vi.fn().mockResolvedValue(0);
|
||||
Reflect.set(smoke, "runStreamingToJobLog", stream);
|
||||
const guestMacos = Reflect.get(smoke, "guestMacos") as (
|
||||
script: string,
|
||||
timeoutMs: number,
|
||||
ctx: { append: (chunk: string) => void },
|
||||
) => Promise<void>;
|
||||
const ctx = { append: vi.fn() };
|
||||
|
||||
await guestMacos.call(smoke, "echo update", 30_000, ctx);
|
||||
|
||||
const call = stream.mock.calls.at(0);
|
||||
expect(call?.[0]).toBe("prlctl");
|
||||
expect(call?.[1]).toEqual([
|
||||
"exec",
|
||||
"macOS Tahoe",
|
||||
"--current-user",
|
||||
"/usr/bin/env",
|
||||
expect.stringMatching(/^PATH=/),
|
||||
"/bin/bash",
|
||||
expect.stringMatching(/^\/tmp\/openclaw-parallels-npm-update-macos-/),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
const log = readFileSync(logPath, "utf8");
|
||||
expect(log).toContain("--current-user whoami");
|
||||
expect(log).toContain("--current-user /usr/bin/env PATH=");
|
||||
expect(log).toContain("/usr/bin/tee /tmp/openclaw-parallels-npm-update-macos-");
|
||||
expect(log).toContain("/bin/chmod 700 /tmp/openclaw-parallels-npm-update-macos-");
|
||||
expect(log).toContain("/usr/sbin/chown desktop-user");
|
||||
});
|
||||
|
||||
it("has a one-command beta validation mode with fresh target coverage", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -841,8 +923,9 @@ exit 1
|
||||
it("keeps macOS sudo fallback update scripts readable by the desktop user", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain('macosExecArgs.indexOf("-u")');
|
||||
expect(script).toContain('"/usr/sbin/chown", sudoUser, scriptPath');
|
||||
expect(script).toContain('"/usr/sbin/chown"');
|
||||
expect(script).toContain("macosUpdateExec.ownerUser");
|
||||
expect(script).toContain("ownerUser: fallbackUser");
|
||||
});
|
||||
|
||||
it("selects macOS desktop users with homes on spaced mounted volumes", () => {
|
||||
@@ -937,6 +1020,16 @@ exit 7
|
||||
);
|
||||
});
|
||||
|
||||
it("writes macOS update scripts through the desktop user transport", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain("const macosUpdateExec = this.resolveMacosUpdateExec(ctx)");
|
||||
expect(script).toContain('{ execArgs: macosUpdateExec.execArgs, mode: "700" }');
|
||||
expect(script).toContain('["exec", vm, ...execArgs, "/usr/bin/tee", scriptPath]');
|
||||
expect(script).toContain('["exec", vm, ...execArgs, "/bin/chmod", mode, scriptPath]');
|
||||
expect(script).toContain("ownerUser: user");
|
||||
});
|
||||
|
||||
it("scrubs future plugin entries before invoking old same-guest updaters", () => {
|
||||
const script = readFileSync(UPDATE_SCRIPTS_PATH, "utf8");
|
||||
const windowsScript = windowsUpdateScript({
|
||||
|
||||
Reference in New Issue
Block a user