mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 23:31:07 +00:00
* fix(daemon): preserve Windows Task Scheduler settings on reinstall and exit early on failed restart * fix(daemon): add test coverage for Create/Change paths, fix early exit grace period * fix(daemon): fix startup-fallback tests for new isRegisteredScheduledTask call * fix(daemon): report early restart failure accurately * fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
252 lines
8.5 KiB
TypeScript
252 lines
8.5 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { PassThrough } from "node:stream";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { installScheduledTask, readScheduledTaskCommand } from "./schtasks.js";
|
|
|
|
const schtasksCalls: string[][] = [];
|
|
const schtasksResponses: { code: number; stdout: string; stderr: string }[] = [];
|
|
|
|
vi.mock("./schtasks-exec.js", () => ({
|
|
execSchtasks: async (argv: string[]) => {
|
|
schtasksCalls.push(argv);
|
|
return schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" };
|
|
},
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
schtasksCalls.length = 0;
|
|
schtasksResponses.length = 0;
|
|
});
|
|
|
|
describe("installScheduledTask", () => {
|
|
async function withUserProfileDir(
|
|
run: (tmpDir: string, env: Record<string, string>) => Promise<void>,
|
|
) {
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-install-"));
|
|
const env = {
|
|
USERPROFILE: tmpDir,
|
|
OPENCLAW_PROFILE: "default",
|
|
};
|
|
try {
|
|
await run(tmpDir, env);
|
|
} finally {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
it("writes quoted set assignments and escapes metacharacters", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
const { scriptPath } = await installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: [
|
|
"node",
|
|
"gateway.js",
|
|
"--display-name",
|
|
"safe&whoami",
|
|
"--percent",
|
|
"%TEMP%",
|
|
"--bang",
|
|
"!token!",
|
|
],
|
|
workingDirectory: "C:\\temp\\poc&calc",
|
|
environment: {
|
|
OC_INJECT: "safe & whoami | calc",
|
|
OC_CARET: "a^b",
|
|
OC_PERCENT: "%TEMP%",
|
|
OC_BANG: "!token!",
|
|
OC_QUOTE: 'he said "hi"',
|
|
OC_EMPTY: "",
|
|
},
|
|
});
|
|
|
|
const script = await fs.readFile(scriptPath, "utf8");
|
|
expect(script).toContain('cd /d "C:\\temp\\poc&calc"');
|
|
expect(script).toContain(
|
|
'node gateway.js --display-name "safe&whoami" --percent "%%TEMP%%" --bang "^!token^!"',
|
|
);
|
|
expect(script).toContain('set "OC_INJECT=safe & whoami | calc"');
|
|
expect(script).toContain('set "OC_CARET=a^^b"');
|
|
expect(script).toContain('set "OC_PERCENT=%%TEMP%%"');
|
|
expect(script).toContain('set "OC_BANG=^!token^!"');
|
|
expect(script).toContain('set "OC_QUOTE=he said ^"hi^""');
|
|
expect(script).not.toContain('set "OC_EMPTY=');
|
|
expect(script).not.toContain("set OC_INJECT=");
|
|
|
|
const parsed = await readScheduledTaskCommand(env);
|
|
expect(parsed).toMatchObject({
|
|
programArguments: [
|
|
"node",
|
|
"gateway.js",
|
|
"--display-name",
|
|
"safe&whoami",
|
|
"--percent",
|
|
"%TEMP%",
|
|
"--bang",
|
|
"!token!",
|
|
],
|
|
workingDirectory: "C:\\temp\\poc&calc",
|
|
});
|
|
expect(parsed?.environment).toMatchObject({
|
|
OC_INJECT: "safe & whoami | calc",
|
|
OC_CARET: "a^b",
|
|
OC_PERCENT: "%TEMP%",
|
|
OC_BANG: "!token!",
|
|
OC_QUOTE: 'he said "hi"',
|
|
});
|
|
expect(parsed?.environment).not.toHaveProperty("OC_EMPTY");
|
|
|
|
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
|
expect(schtasksCalls[1]).toEqual(["/Query", "/TN", "OpenClaw Gateway"]);
|
|
expect(schtasksCalls[2]?.[0]).toBe("/Change");
|
|
expect(schtasksCalls[3]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
|
});
|
|
});
|
|
|
|
it("rejects line breaks in command arguments, env vars, and descriptions", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
await expect(
|
|
installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js", "bad\narg"],
|
|
environment: {},
|
|
}),
|
|
).rejects.toThrow(/Command argument cannot contain CR or LF/);
|
|
|
|
await expect(
|
|
installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: { BAD: "line1\r\nline2" },
|
|
}),
|
|
).rejects.toThrow(/Environment variable value cannot contain CR or LF/);
|
|
|
|
await expect(
|
|
installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
description: "bad\ndescription",
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: {},
|
|
}),
|
|
).rejects.toThrow(/Task description cannot contain CR or LF/);
|
|
});
|
|
});
|
|
|
|
it("uses /Create when the task does not exist yet", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "ERROR: The system cannot find the file specified." },
|
|
);
|
|
|
|
await installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: {},
|
|
});
|
|
|
|
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
|
expect(schtasksCalls[1]).toEqual(["/Query", "/TN", "OpenClaw Gateway"]);
|
|
expect(schtasksCalls[2]?.[0]).toBe("/Create");
|
|
expect(schtasksCalls[3]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
|
});
|
|
});
|
|
|
|
it("falls back to /Create when /Change fails on an existing task", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "ERROR: Access is denied." },
|
|
);
|
|
|
|
await installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: {},
|
|
});
|
|
|
|
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
|
expect(schtasksCalls[1]).toEqual(["/Query", "/TN", "OpenClaw Gateway"]);
|
|
expect(schtasksCalls[2]?.[0]).toBe("/Change");
|
|
expect(schtasksCalls[3]?.[0]).toBe("/Create");
|
|
expect(schtasksCalls[4]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
|
});
|
|
});
|
|
|
|
it("throws when /Run fails after updating an existing task", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "ERROR: Access is denied." },
|
|
);
|
|
|
|
await expect(
|
|
installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: {},
|
|
}),
|
|
).rejects.toThrow("schtasks run failed: ERROR: Access is denied.");
|
|
|
|
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
|
expect(schtasksCalls[1]).toEqual(["/Query", "/TN", "OpenClaw Gateway"]);
|
|
expect(schtasksCalls[2]?.[0]).toBe("/Change");
|
|
expect(schtasksCalls[3]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
|
});
|
|
});
|
|
|
|
it("throws when /Run fails after creating a new task", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "ERROR: The system cannot find the file specified." },
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "ERROR: Access is denied." },
|
|
);
|
|
|
|
await expect(
|
|
installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: {},
|
|
}),
|
|
).rejects.toThrow("schtasks run failed: ERROR: Access is denied.");
|
|
|
|
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
|
expect(schtasksCalls[1]).toEqual(["/Query", "/TN", "OpenClaw Gateway"]);
|
|
expect(schtasksCalls[2]?.[0]).toBe("/Create");
|
|
expect(schtasksCalls[3]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
|
});
|
|
});
|
|
|
|
it("does not persist a frozen PATH snapshot into the generated task script", async () => {
|
|
await withUserProfileDir(async (_tmpDir, env) => {
|
|
const { scriptPath } = await installScheduledTask({
|
|
env,
|
|
stdout: new PassThrough(),
|
|
programArguments: ["node", "gateway.js"],
|
|
environment: {
|
|
PATH: "C:\\Windows\\System32;C:\\Program Files\\Docker\\Docker\\resources\\bin",
|
|
OPENCLAW_GATEWAY_PORT: "18789",
|
|
},
|
|
});
|
|
|
|
const script = await fs.readFile(scriptPath, "utf8");
|
|
expect(script).not.toContain('set "PATH=');
|
|
expect(script).toContain('set "OPENCLAW_GATEWAY_PORT=18789"');
|
|
});
|
|
});
|
|
});
|