agent-orchestrator/packages/cli/__tests__/lifecycle-service.test.ts

95 lines
3.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import * as fs from "node:fs";
import * as aoCore from "@composio/ao-core";
import type { OrchestratorConfig } from "@composio/ao-core";
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
readFileSync: vi.fn(),
unlinkSync: vi.fn(),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
openSync: vi.fn(),
closeSync: vi.fn(),
}));
vi.mock("@composio/ao-core", async (importOriginal) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importOriginal<typeof import("@composio/ao-core")>();
return {
...actual,
getProjectBaseDir: vi.fn(() => "/mock/project"),
killProcessTree: vi.fn(() => Promise.resolve()),
};
});
const config: OrchestratorConfig = {
configPath: "/mock/config.yaml",
projects: { "my-project": { path: "." } },
} as unknown as OrchestratorConfig;
describe("stopLifecycleWorker", () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(aoCore.getProjectBaseDir).mockReturnValue("/mock/project");
vi.mocked(aoCore.killProcessTree).mockResolvedValue(undefined);
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("12345\n");
});
it("returns false and skips kill when PID is stale (process died between checks)", async () => {
// First call: getLifecycleWorkerStatus sees a live process
// Second call: our explicit check sees it's already dead (race)
let callCount = 0;
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => {
callCount++;
if (callCount === 1) return true;
throw Object.assign(new Error("no such process"), { code: "ESRCH" });
});
const { stopLifecycleWorker } = await import("../src/lib/lifecycle-service.js");
const result = await stopLifecycleWorker(config, "my-project");
expect(result).toBe(false);
expect(aoCore.killProcessTree).not.toHaveBeenCalled();
killSpy.mockRestore();
});
it("sends SIGTERM and returns true when process exits within timeout", async () => {
// Calls: 1=getLifecycleWorkerStatus, 2=explicit check, 3+=polling loop
let callCount = 0;
const killSpy = vi.spyOn(process, "kill").mockImplementation(() => {
callCount++;
if (callCount <= 2) return true; // alive
throw Object.assign(new Error("no such process"), { code: "ESRCH" }); // dead after SIGTERM
});
const { stopLifecycleWorker } = await import("../src/lib/lifecycle-service.js");
const result = await stopLifecycleWorker(config, "my-project");
expect(result).toBe(true);
expect(aoCore.killProcessTree).toHaveBeenCalledWith(12345, "SIGTERM");
expect(aoCore.killProcessTree).not.toHaveBeenCalledWith(12345, "SIGKILL");
killSpy.mockRestore();
});
it("sends SIGKILL when process does not exit within timeout", async () => {
vi.useFakeTimers();
const killSpy = vi.spyOn(process, "kill").mockReturnValue(true); // process never dies
const { stopLifecycleWorker } = await import("../src/lib/lifecycle-service.js");
const promise = stopLifecycleWorker(config, "my-project");
// Advance past the 5s timeout
await vi.advanceTimersByTimeAsync(6000);
const result = await promise;
expect(result).toBe(true);
expect(aoCore.killProcessTree).toHaveBeenCalledWith(12345, "SIGKILL");
vi.useRealTimers();
killSpy.mockRestore();
});
});