mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 20:40:45 +00:00
* Gateway: treat scope-limited probe RPC as degraded * Docs: clarify gateway probe degraded scope output * test: fix CI type regressions in gateway and outbound suites * Tests: fix Node24 diffs theme loading and Windows assertions * Tests: fix extension typing after main rebase * Tests: fix Windows CI regressions after rebase * Tests: normalize executable path assertions on Windows * Tests: remove duplicate gateway daemon result alias * Tests: stabilize Windows approval path assertions * Tests: fix Discord rate-limit startup fixture typing * Tests: use Windows-friendly relative exec fixtures --------- Co-authored-by: Mainframe <mainframe@MainfraacStudio.localdomain>
229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { PassThrough } from "node:stream";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { quoteCmdScriptArg } from "./cmd-argv.js";
|
|
import "./test-helpers/schtasks-base-mocks.js";
|
|
import {
|
|
inspectPortUsage,
|
|
killProcessTree,
|
|
resetSchtasksBaseMocks,
|
|
schtasksResponses,
|
|
withWindowsEnv,
|
|
writeGatewayScript,
|
|
} from "./test-helpers/schtasks-fixtures.js";
|
|
const childUnref = vi.hoisted(() => vi.fn());
|
|
const spawn = vi.hoisted(() => vi.fn(() => ({ unref: childUnref })));
|
|
|
|
vi.mock("node:child_process", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:child_process")>();
|
|
return {
|
|
...actual,
|
|
spawn,
|
|
};
|
|
});
|
|
|
|
const {
|
|
installScheduledTask,
|
|
isScheduledTaskInstalled,
|
|
readScheduledTaskRuntime,
|
|
restartScheduledTask,
|
|
resolveTaskScriptPath,
|
|
stopScheduledTask,
|
|
} = await import("./schtasks.js");
|
|
|
|
function resolveStartupEntryPath(env: Record<string, string>) {
|
|
return path.join(
|
|
env.APPDATA,
|
|
"Microsoft",
|
|
"Windows",
|
|
"Start Menu",
|
|
"Programs",
|
|
"Startup",
|
|
"OpenClaw Gateway.cmd",
|
|
);
|
|
}
|
|
|
|
async function writeStartupFallbackEntry(env: Record<string, string>) {
|
|
const startupEntryPath = resolveStartupEntryPath(env);
|
|
await fs.mkdir(path.dirname(startupEntryPath), { recursive: true });
|
|
await fs.writeFile(startupEntryPath, "@echo off\r\n", "utf8");
|
|
return startupEntryPath;
|
|
}
|
|
|
|
function expectStartupFallbackSpawn(env: Record<string, string>) {
|
|
expect(spawn).toHaveBeenCalledWith(
|
|
"cmd.exe",
|
|
["/d", "/s", "/c", quoteCmdScriptArg(resolveTaskScriptPath(env))],
|
|
expect.objectContaining({ detached: true, stdio: "ignore", windowsHide: true }),
|
|
);
|
|
}
|
|
|
|
function expectGatewayTermination(pid: number) {
|
|
if (process.platform === "win32") {
|
|
expect(killProcessTree).not.toHaveBeenCalled();
|
|
return;
|
|
}
|
|
expect(killProcessTree).toHaveBeenCalledWith(pid, { graceMs: 300 });
|
|
}
|
|
|
|
function addStartupFallbackMissingResponses(
|
|
extraResponses: Array<{ code: number; stdout: string; stderr: string }> = [],
|
|
) {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "not found" },
|
|
...extraResponses,
|
|
);
|
|
}
|
|
beforeEach(() => {
|
|
resetSchtasksBaseMocks();
|
|
spawn.mockClear();
|
|
childUnref.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("Windows startup fallback", () => {
|
|
it("falls back to a Startup-folder launcher when schtasks create is denied", async () => {
|
|
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 5, stdout: "", stderr: "ERROR: Access is denied." },
|
|
);
|
|
|
|
const stdout = new PassThrough();
|
|
let printed = "";
|
|
stdout.on("data", (chunk) => {
|
|
printed += String(chunk);
|
|
});
|
|
|
|
const result = await installScheduledTask({
|
|
env,
|
|
stdout,
|
|
programArguments: ["node", "gateway.js", "--port", "18789"],
|
|
environment: { OPENCLAW_GATEWAY_PORT: "18789" },
|
|
});
|
|
|
|
const startupEntryPath = resolveStartupEntryPath(env);
|
|
const startupScript = await fs.readFile(startupEntryPath, "utf8");
|
|
expect(result.scriptPath).toBe(resolveTaskScriptPath(env));
|
|
expect(startupScript).toContain('start "" /min cmd.exe /d /c');
|
|
expect(startupScript).toContain("gateway.cmd");
|
|
expect(spawn).toHaveBeenCalledWith(
|
|
"cmd.exe",
|
|
["/d", "/s", "/c", quoteCmdScriptArg(resolveTaskScriptPath(env))],
|
|
expect.objectContaining({ detached: true, stdio: "ignore", windowsHide: true }),
|
|
);
|
|
expect(childUnref).toHaveBeenCalled();
|
|
expect(printed).toContain("Installed Windows login item");
|
|
});
|
|
});
|
|
|
|
it("falls back to a Startup-folder launcher when schtasks create hangs", async () => {
|
|
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
|
schtasksResponses.push(
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 124, stdout: "", stderr: "schtasks timed out after 15000ms" },
|
|
);
|
|
|
|
const stdout = new PassThrough();
|
|
await installScheduledTask({
|
|
env,
|
|
stdout,
|
|
programArguments: ["node", "gateway.js", "--port", "18789"],
|
|
environment: { OPENCLAW_GATEWAY_PORT: "18789" },
|
|
});
|
|
|
|
await expect(fs.access(resolveStartupEntryPath(env))).resolves.toBeUndefined();
|
|
expectStartupFallbackSpawn(env);
|
|
});
|
|
});
|
|
|
|
it("treats an installed Startup-folder launcher as loaded", async () => {
|
|
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
|
addStartupFallbackMissingResponses();
|
|
await writeStartupFallbackEntry(env);
|
|
|
|
await expect(isScheduledTaskInstalled({ env })).resolves.toBe(true);
|
|
});
|
|
});
|
|
|
|
it("reports runtime from the gateway listener when using the Startup fallback", async () => {
|
|
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
|
addStartupFallbackMissingResponses();
|
|
await writeStartupFallbackEntry(env);
|
|
inspectPortUsage.mockResolvedValue({
|
|
port: 18789,
|
|
status: "busy",
|
|
listeners: [{ pid: 4242, command: "node.exe" }],
|
|
hints: [],
|
|
});
|
|
|
|
await expect(readScheduledTaskRuntime(env)).resolves.toMatchObject({
|
|
status: "running",
|
|
pid: 4242,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("restarts the Startup fallback by killing the current pid and relaunching the entry", async () => {
|
|
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
|
addStartupFallbackMissingResponses([
|
|
{ code: 0, stdout: "", stderr: "" },
|
|
{ code: 1, stdout: "", stderr: "not found" },
|
|
]);
|
|
await writeStartupFallbackEntry(env);
|
|
inspectPortUsage.mockResolvedValue({
|
|
port: 18789,
|
|
status: "busy",
|
|
listeners: [{ pid: 5151, command: "node.exe" }],
|
|
hints: [],
|
|
});
|
|
|
|
const stdout = new PassThrough();
|
|
await expect(restartScheduledTask({ env, stdout })).resolves.toEqual({
|
|
outcome: "completed",
|
|
});
|
|
expectGatewayTermination(5151);
|
|
expectStartupFallbackSpawn(env);
|
|
});
|
|
});
|
|
|
|
it("kills the Startup fallback runtime even when the CLI env omits the gateway port", async () => {
|
|
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
|
schtasksResponses.push({ code: 0, stdout: "", stderr: "" });
|
|
await writeGatewayScript(env);
|
|
await writeStartupFallbackEntry(env);
|
|
inspectPortUsage
|
|
.mockResolvedValueOnce({
|
|
port: 18789,
|
|
status: "busy",
|
|
listeners: [{ pid: 5151, command: "node.exe" }],
|
|
hints: [],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
port: 18789,
|
|
status: "busy",
|
|
listeners: [{ pid: 5151, command: "node.exe" }],
|
|
hints: [],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
port: 18789,
|
|
status: "free",
|
|
listeners: [],
|
|
hints: [],
|
|
});
|
|
|
|
const stdout = new PassThrough();
|
|
const envWithoutPort = { ...env };
|
|
delete envWithoutPort.OPENCLAW_GATEWAY_PORT;
|
|
await stopScheduledTask({ env: envWithoutPort, stdout });
|
|
|
|
expectGatewayTermination(5151);
|
|
});
|
|
});
|
|
});
|