refactor: share windows daemon test fixtures

This commit is contained in:
Peter Steinberger 2026-03-13 19:22:43 +00:00
parent 4e05357c45
commit 7d69579634
7 changed files with 172 additions and 149 deletions

View File

@ -1,30 +1,15 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
defaultRuntime,
resetLifecycleRuntimeLogs,
resetLifecycleServiceMocks,
service,
stubEmptyGatewayEnv,
} from "./test-helpers/lifecycle-core-harness.js";
const readConfigFileSnapshotMock = vi.fn();
const loadConfig = vi.fn(() => ({}));
const runtimeLogs: string[] = [];
const defaultRuntime = {
log: (message: string) => runtimeLogs.push(message),
error: vi.fn(),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
};
const service = {
label: "TestService",
loadedText: "loaded",
notLoadedText: "not loaded",
install: vi.fn(),
uninstall: vi.fn(),
stop: vi.fn(),
isLoaded: vi.fn(),
readCommand: vi.fn(),
readRuntime: vi.fn(),
restart: vi.fn(),
};
vi.mock("../../config/config.js", () => ({
loadConfig: () => loadConfig(),
readConfigFileSnapshot: () => readConfigFileSnapshotMock(),
@ -50,7 +35,7 @@ describe("runServiceRestart config pre-flight (#35862)", () => {
});
beforeEach(() => {
runtimeLogs.length = 0;
resetLifecycleRuntimeLogs();
readConfigFileSnapshotMock.mockReset();
readConfigFileSnapshotMock.mockResolvedValue({
exists: true,
@ -60,15 +45,8 @@ describe("runServiceRestart config pre-flight (#35862)", () => {
});
loadConfig.mockReset();
loadConfig.mockReturnValue({});
service.isLoaded.mockClear();
service.readCommand.mockClear();
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({ environment: {} });
service.restart.mockResolvedValue({ outcome: "completed" });
vi.unstubAllEnvs();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
resetLifecycleServiceMocks();
stubEmptyGatewayEnv();
});
it("aborts restart when config is invalid", async () => {
@ -152,7 +130,7 @@ describe("runServiceStart config pre-flight (#35862)", () => {
});
beforeEach(() => {
runtimeLogs.length = 0;
resetLifecycleRuntimeLogs();
readConfigFileSnapshotMock.mockReset();
readConfigFileSnapshotMock.mockResolvedValue({
exists: true,
@ -160,10 +138,7 @@ describe("runServiceStart config pre-flight (#35862)", () => {
config: {},
issues: [],
});
service.isLoaded.mockClear();
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
service.restart.mockResolvedValue({ outcome: "completed" });
resetLifecycleServiceMocks();
});
it("aborts start when config is invalid", async () => {

View File

@ -1,4 +1,12 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
defaultRuntime,
resetLifecycleRuntimeLogs,
resetLifecycleServiceMocks,
runtimeLogs,
service,
stubEmptyGatewayEnv,
} from "./test-helpers/lifecycle-core-harness.js";
const loadConfig = vi.fn(() => ({
gateway: {
@ -8,28 +16,6 @@ const loadConfig = vi.fn(() => ({
},
}));
const runtimeLogs: string[] = [];
const defaultRuntime = {
log: (message: string) => runtimeLogs.push(message),
error: vi.fn(),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
};
const service = {
label: "TestService",
loadedText: "loaded",
notLoadedText: "not loaded",
install: vi.fn(),
uninstall: vi.fn(),
stop: vi.fn(),
isLoaded: vi.fn(),
readCommand: vi.fn(),
readRuntime: vi.fn(),
restart: vi.fn(),
};
vi.mock("../../config/config.js", () => ({
loadConfig: () => loadConfig(),
readBestEffortConfig: async () => loadConfig(),
@ -49,7 +35,7 @@ describe("runServiceRestart token drift", () => {
});
beforeEach(() => {
runtimeLogs.length = 0;
resetLifecycleRuntimeLogs();
loadConfig.mockReset();
loadConfig.mockReturnValue({
gateway: {
@ -58,19 +44,11 @@ describe("runServiceRestart token drift", () => {
},
},
});
service.isLoaded.mockClear();
service.readCommand.mockClear();
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
resetLifecycleServiceMocks();
service.readCommand.mockResolvedValue({
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
});
service.restart.mockResolvedValue({ outcome: "completed" });
vi.unstubAllEnvs();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
vi.stubEnv("OPENCLAW_GATEWAY_URL", "");
vi.stubEnv("CLAWDBOT_GATEWAY_URL", "");
stubEmptyGatewayEnv();
});
it("emits drift warning when enabled", async () => {

View File

@ -0,0 +1,45 @@
import { vi } from "vitest";
export const runtimeLogs: string[] = [];
export const defaultRuntime = {
log: (message: string) => runtimeLogs.push(message),
error: vi.fn(),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
};
export const service = {
label: "TestService",
loadedText: "loaded",
notLoadedText: "not loaded",
install: vi.fn(),
uninstall: vi.fn(),
stop: vi.fn(),
isLoaded: vi.fn(),
readCommand: vi.fn(),
readRuntime: vi.fn(),
restart: vi.fn(),
};
export function resetLifecycleRuntimeLogs() {
runtimeLogs.length = 0;
}
export function resetLifecycleServiceMocks() {
service.isLoaded.mockClear();
service.readCommand.mockClear();
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({ environment: {} });
service.restart.mockResolvedValue({ outcome: "completed" });
}
export function stubEmptyGatewayEnv() {
vi.unstubAllEnvs();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
vi.stubEnv("OPENCLAW_GATEWAY_URL", "");
vi.stubEnv("CLAWDBOT_GATEWAY_URL", "");
}

View File

@ -1,34 +1,19 @@
import fs from "node:fs/promises";
import os from "node:os";
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";
const schtasksResponses = vi.hoisted(
() => [] as Array<{ code: number; stdout: string; stderr: string }>,
);
const schtasksCalls = vi.hoisted(() => [] as string[][]);
const inspectPortUsage = vi.hoisted(() => vi.fn());
const killProcessTree = vi.hoisted(() => vi.fn());
import "./test-helpers/schtasks-base-mocks.js";
import {
inspectPortUsage,
killProcessTree,
resetSchtasksBaseMocks,
schtasksResponses,
withWindowsEnv,
} from "./test-helpers/schtasks-fixtures.js";
const childUnref = vi.hoisted(() => vi.fn());
const spawn = vi.hoisted(() => vi.fn(() => ({ unref: childUnref })));
vi.mock("./schtasks-exec.js", () => ({
execSchtasks: async (argv: string[]) => {
schtasksCalls.push(argv);
return schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" };
},
}));
vi.mock("../infra/ports.js", () => ({
inspectPortUsage: (...args: unknown[]) => inspectPortUsage(...args),
}));
vi.mock("../process/kill-tree.js", () => ({
killProcessTree: (...args: unknown[]) => killProcessTree(...args),
}));
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
@ -58,6 +43,7 @@ function resolveStartupEntryPath(env: Record<string, string>) {
);
}
<<<<<<< HEAD
async function withWindowsEnv(
run: (params: { tmpDir: string; env: Record<string, string> }) => Promise<void>,
) {
@ -90,11 +76,28 @@ async function writeGatewayScript(env: Record<string, string>, port = 18789) {
);
}
||||||| parent of 8fb2c3f894 (refactor: share windows daemon test fixtures)
async function withWindowsEnv(
run: (params: { tmpDir: string; env: Record<string, string> }) => Promise<void>,
) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-win-startup-"));
const env = {
USERPROFILE: tmpDir,
APPDATA: path.join(tmpDir, "AppData", "Roaming"),
OPENCLAW_PROFILE: "default",
OPENCLAW_GATEWAY_PORT: "18789",
};
try {
await run({ tmpDir, env });
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}
=======
>>>>>>> 8fb2c3f894 (refactor: share windows daemon test fixtures)
beforeEach(() => {
schtasksResponses.length = 0;
schtasksCalls.length = 0;
inspectPortUsage.mockReset();
killProcessTree.mockReset();
resetSchtasksBaseMocks();
spawn.mockClear();
childUnref.mockClear();
});
@ -105,7 +108,7 @@ afterEach(() => {
describe("Windows startup fallback", () => {
it("falls back to a Startup-folder launcher when schtasks create is denied", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 5, stdout: "", stderr: "ERROR: Access is denied." },
@ -140,7 +143,7 @@ describe("Windows startup fallback", () => {
});
it("falls back to a Startup-folder launcher when schtasks create hangs", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 124, stdout: "", stderr: "schtasks timed out after 15000ms" },
@ -164,7 +167,7 @@ describe("Windows startup fallback", () => {
});
it("treats an installed Startup-folder launcher as loaded", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 1, stdout: "", stderr: "not found" },
@ -177,7 +180,7 @@ describe("Windows startup fallback", () => {
});
it("reports runtime from the gateway listener when using the Startup fallback", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 1, stdout: "", stderr: "not found" },
@ -199,7 +202,7 @@ describe("Windows startup fallback", () => {
});
it("restarts the Startup fallback by killing the current pid and relaunching the entry", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 1, stdout: "", stderr: "not found" },

View File

@ -1,34 +1,20 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const schtasksResponses = vi.hoisted(
() => [] as Array<{ code: number; stdout: string; stderr: string }>,
);
const schtasksCalls = vi.hoisted(() => [] as string[][]);
const inspectPortUsage = vi.hoisted(() => vi.fn());
const killProcessTree = vi.hoisted(() => vi.fn());
import "./test-helpers/schtasks-base-mocks.js";
import {
inspectPortUsage,
killProcessTree,
resetSchtasksBaseMocks,
schtasksCalls,
schtasksResponses,
withWindowsEnv,
} from "./test-helpers/schtasks-fixtures.js";
const findVerifiedGatewayListenerPidsOnPortSync = vi.hoisted(() =>
vi.fn<(port: number) => number[]>(() => []),
);
vi.mock("./schtasks-exec.js", () => ({
execSchtasks: async (argv: string[]) => {
schtasksCalls.push(argv);
return schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" };
},
}));
vi.mock("../infra/ports.js", () => ({
inspectPortUsage: (...args: unknown[]) => inspectPortUsage(...args),
}));
vi.mock("../process/kill-tree.js", () => ({
killProcessTree: (...args: unknown[]) => killProcessTree(...args),
}));
vi.mock("../infra/gateway-processes.js", () => ({
findVerifiedGatewayListenerPidsOnPortSync: (port: number) =>
findVerifiedGatewayListenerPidsOnPortSync(port),
@ -37,23 +23,6 @@ vi.mock("../infra/gateway-processes.js", () => ({
const { restartScheduledTask, resolveTaskScriptPath, stopScheduledTask } =
await import("./schtasks.js");
async function withWindowsEnv(
run: (params: { tmpDir: string; env: Record<string, string> }) => Promise<void>,
) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-win-stop-"));
const env = {
USERPROFILE: tmpDir,
APPDATA: path.join(tmpDir, "AppData", "Roaming"),
OPENCLAW_PROFILE: "default",
OPENCLAW_GATEWAY_PORT: "18789",
};
try {
await run({ tmpDir, env });
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}
async function writeGatewayScript(env: Record<string, string>, port = 18789) {
const scriptPath = resolveTaskScriptPath(env);
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
@ -70,10 +39,7 @@ async function writeGatewayScript(env: Record<string, string>, port = 18789) {
}
beforeEach(() => {
schtasksResponses.length = 0;
schtasksCalls.length = 0;
inspectPortUsage.mockReset();
killProcessTree.mockReset();
resetSchtasksBaseMocks();
findVerifiedGatewayListenerPidsOnPortSync.mockReset();
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
inspectPortUsage.mockResolvedValue({
@ -90,7 +56,7 @@ afterEach(() => {
describe("Scheduled Task stop/restart cleanup", () => {
it("kills lingering verified gateway listeners after schtasks stop", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => {
await writeGatewayScript(env);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
@ -168,7 +134,7 @@ describe("Scheduled Task stop/restart cleanup", () => {
});
it("falls back to inspected gateway listeners when sync verification misses on Windows", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => {
await writeGatewayScript(env);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
@ -206,7 +172,7 @@ describe("Scheduled Task stop/restart cleanup", () => {
});
it("kills lingering verified gateway listeners and waits for port release before restart", async () => {
await withWindowsEnv(async ({ env }) => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => {
await writeGatewayScript(env);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },

View File

@ -0,0 +1,22 @@
import { vi } from "vitest";
import {
inspectPortUsage,
killProcessTree,
schtasksCalls,
schtasksResponses,
} from "./schtasks-fixtures.js";
vi.mock("../schtasks-exec.js", () => ({
execSchtasks: async (argv: string[]) => {
schtasksCalls.push(argv);
return schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" };
},
}));
vi.mock("../../infra/ports.js", () => ({
inspectPortUsage: (...args: unknown[]) => inspectPortUsage(...args),
}));
vi.mock("../../process/kill-tree.js", () => ({
killProcessTree: (...args: unknown[]) => killProcessTree(...args),
}));

View File

@ -0,0 +1,34 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { vi } from "vitest";
export const schtasksResponses: Array<{ code: number; stdout: string; stderr: string }> = [];
export const schtasksCalls: string[][] = [];
export const inspectPortUsage = vi.fn();
export const killProcessTree = vi.fn();
export async function withWindowsEnv(
prefix: string,
run: (params: { tmpDir: string; env: Record<string, string> }) => Promise<void>,
) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
const env = {
USERPROFILE: tmpDir,
APPDATA: path.join(tmpDir, "AppData", "Roaming"),
OPENCLAW_PROFILE: "default",
OPENCLAW_GATEWAY_PORT: "18789",
};
try {
await run({ tmpDir, env });
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}
export function resetSchtasksBaseMocks() {
schtasksResponses.length = 0;
schtasksCalls.length = 0;
inspectPortUsage.mockReset();
killProcessTree.mockReset();
}