| import fs from "node:fs/promises"; |
| import os from "node:os"; |
| import path from "node:path"; |
| import { afterEach, describe, expect, it, vi } from "vitest"; |
| import { |
| checkInteractionLock, |
| createInteractionLock, |
| refreshInteractionLock, |
| sleep, |
| startAutonomousDaemon, |
| } from "./daemon-cooperative.js"; |
|
|
| const tmpDirs: string[] = []; |
|
|
| async function createWorkspaceRoot() { |
| const root = await fs.mkdtemp(path.join(os.tmpdir(), "openskynet-omega-daemon-")); |
| tmpDirs.push(root); |
| return root; |
| } |
|
|
| afterEach(async () => { |
| vi.restoreAllMocks(); |
| await Promise.all( |
| tmpDirs.splice(0, tmpDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })), |
| ); |
| }); |
|
|
| describe("omega daemon cooperative", () => { |
| it("writes structured interaction lock payloads", async () => { |
| const workspaceRoot = await createWorkspaceRoot(); |
|
|
| await createInteractionLock(workspaceRoot); |
|
|
| const raw = await fs.readFile(path.join(workspaceRoot, ".interaction-lock"), "utf-8"); |
| const parsed = JSON.parse(raw) as { owner: string; pid: number; refreshedAt: number }; |
| expect(parsed).toMatchObject({ |
| owner: "tui", |
| pid: process.pid, |
| refreshedAt: expect.any(Number), |
| }); |
| }); |
|
|
| it("refreshInteractionLock updates the lock timestamp until cleanup", async () => { |
| const workspaceRoot = await createWorkspaceRoot(); |
|
|
| const stop = await refreshInteractionLock(workspaceRoot, 20); |
| const lockPath = path.join(workspaceRoot, ".interaction-lock"); |
| const first = JSON.parse(await fs.readFile(lockPath, "utf-8")) as { refreshedAt: number }; |
|
|
| await sleep(40); |
| const second = JSON.parse(await fs.readFile(lockPath, "utf-8")) as { refreshedAt: number }; |
| expect(second.refreshedAt).toBeGreaterThanOrEqual(first.refreshedAt); |
|
|
| await stop(); |
| await expect(fs.stat(lockPath)).rejects.toThrow(); |
| }); |
|
|
| it("treats stale locks as inactive and deletes them", async () => { |
| const workspaceRoot = await createWorkspaceRoot(); |
| const lockPath = path.join(workspaceRoot, ".interaction-lock"); |
| await fs.writeFile(lockPath, "{}\n", "utf-8"); |
| const staleSecondsAgo = Date.now() - 24 * 60 * 60 * 1000; |
| await fs.utimes(lockPath, staleSecondsAgo / 1000, staleSecondsAgo / 1000); |
|
|
| await expect(checkInteractionLock(lockPath)).resolves.toBe(false); |
| await expect(fs.stat(lockPath)).rejects.toThrow(); |
| }); |
|
|
| it("supports cooperative shutdown via AbortSignal", async () => { |
| const workspaceRoot = await createWorkspaceRoot(); |
| const controller = new AbortController(); |
| const runHeartbeatCycle = vi.fn(async () => { |
| controller.abort(new Error("stop")); |
| }); |
|
|
| await startAutonomousDaemon( |
| { workspaceRoot, sessionKey: "main", signal: controller.signal }, |
| { |
| checkInteractionLock: vi.fn(async () => false), |
| runHeartbeatCycle, |
| sleep: vi.fn(async (_ms, signal) => { |
| if (signal?.aborted) throw signal.reason; |
| }), |
| log: vi.fn(), |
| error: vi.fn(), |
| }, |
| ); |
|
|
| expect(runHeartbeatCycle).toHaveBeenCalledTimes(1); |
| }); |
| }); |
|
|