mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 12:11:20 +00:00
test(infra): reuse temp dir helper in state and watch tests
This commit is contained in:
@@ -1,14 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { migrateOrphanedSessionKeys } from "./state-migrations.js";
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "orphan-keys-test-"));
|
||||
}
|
||||
|
||||
function writeStore(storePath: string, store: Record<string, unknown>): void {
|
||||
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
||||
fs.writeFileSync(storePath, JSON.stringify(store));
|
||||
@@ -18,189 +14,202 @@ function readStore(storePath: string): Record<string, unknown> {
|
||||
return JSON.parse(fs.readFileSync(storePath, "utf-8"));
|
||||
}
|
||||
|
||||
describe("migrateOrphanedSessionKeys", () => {
|
||||
let tmpDir: string;
|
||||
let stateDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = makeTmpDir();
|
||||
stateDir = path.join(tmpDir, ".openclaw");
|
||||
async function withStateFixture(
|
||||
run: (params: { tmpDir: string; stateDir: string }) => Promise<void>,
|
||||
): Promise<void> {
|
||||
await withTempDir({ prefix: "orphan-keys-test-" }, async (tmpDir) => {
|
||||
const stateDir = path.join(tmpDir, ".openclaw");
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
await run({ tmpDir, stateDir });
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("migrateOrphanedSessionKeys", () => {
|
||||
it("renames orphaned raw key to canonical form", async () => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
await withStateFixture(async ({ stateDir }) => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes.length).toBeGreaterThan(0);
|
||||
const store = readStore(storePath);
|
||||
expect(store["agent:ops:work"]).toBeDefined();
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("abc-123");
|
||||
expect(store["agent:main:main"]).toBeUndefined();
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes.length).toBeGreaterThan(0);
|
||||
const store = readStore(storePath);
|
||||
expect(store["agent:ops:work"]).toBeDefined();
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("abc-123");
|
||||
expect(store["agent:main:main"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps most recently updated entry when both orphan and canonical exist", async () => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "old-orphan", updatedAt: 500 },
|
||||
"agent:ops:work": { sessionId: "current", updatedAt: 2000 },
|
||||
await withStateFixture(async ({ stateDir }) => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "old-orphan", updatedAt: 500 },
|
||||
"agent:ops:work": { sessionId: "current", updatedAt: 2000 },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
const store = readStore(storePath);
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("current");
|
||||
expect(store["agent:main:main"]).toBeUndefined();
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
const store = readStore(storePath);
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("current");
|
||||
expect(store["agent:main:main"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips stores that are already fully canonical", async () => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:ops:work": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
await withStateFixture(async ({ stateDir }) => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:ops:work": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles missing store files gracefully", async () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
await withStateFixture(async ({ stateDir }) => {
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(result.changes).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("is idempotent — running twice produces same result", async () => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
await withStateFixture(async ({ stateDir }) => {
|
||||
const storePath = path.join(stateDir, "agents", "ops", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const env = { OPENCLAW_STATE_DIR: stateDir };
|
||||
await migrateOrphanedSessionKeys({ cfg, env });
|
||||
const result2 = await migrateOrphanedSessionKeys({ cfg, env });
|
||||
|
||||
expect(result2.changes).toHaveLength(0);
|
||||
const store = readStore(storePath);
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("abc-123");
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const env = { OPENCLAW_STATE_DIR: stateDir };
|
||||
await migrateOrphanedSessionKeys({ cfg, env });
|
||||
const result2 = await migrateOrphanedSessionKeys({ cfg, env });
|
||||
|
||||
expect(result2.changes).toHaveLength(0);
|
||||
const store = readStore(storePath);
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("abc-123");
|
||||
});
|
||||
|
||||
it("preserves legitimate agent:main:* keys in shared stores with both main and non-main agents", async () => {
|
||||
// When session.store lacks {agentId}, all agents resolve to the same file.
|
||||
// The "main" agent's keys must not be remapped into the "ops" namespace.
|
||||
const sharedStorePath = path.join(tmpDir, "shared-sessions.json");
|
||||
writeStore(sharedStorePath, {
|
||||
"agent:main:main": { sessionId: "main-session", updatedAt: 2000 },
|
||||
"agent:ops:work": { sessionId: "ops-session", updatedAt: 1000 },
|
||||
await withStateFixture(async ({ tmpDir, stateDir }) => {
|
||||
// When session.store lacks {agentId}, all agents resolve to the same file.
|
||||
// The "main" agent's keys must not be remapped into the "ops" namespace.
|
||||
const sharedStorePath = path.join(tmpDir, "shared-sessions.json");
|
||||
writeStore(sharedStorePath, {
|
||||
"agent:main:main": { sessionId: "main-session", updatedAt: 2000 },
|
||||
"agent:ops:work": { sessionId: "ops-session", updatedAt: 1000 },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work", store: sharedStorePath },
|
||||
agents: { list: [{ id: "main" }, { id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
const store = readStore(sharedStorePath);
|
||||
// main agent's session is canonicalised to use configured mainKey ("work"),
|
||||
// but stays in the "main" agent namespace — NOT remapped into "ops".
|
||||
expect(store["agent:main:work"]).toBeDefined();
|
||||
expect((store["agent:main:work"] as { sessionId: string }).sessionId).toBe("main-session");
|
||||
expect(store["agent:ops:work"]).toBeDefined();
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("ops-session");
|
||||
// The key must NOT have been merged into ops namespace
|
||||
expect(Object.keys(store).filter((k) => k.startsWith("agent:ops:")).length).toBe(1);
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work", store: sharedStorePath },
|
||||
agents: { list: [{ id: "main" }, { id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
const store = readStore(sharedStorePath);
|
||||
// main agent's session is canonicalised to use configured mainKey ("work"),
|
||||
// but stays in the "main" agent namespace — NOT remapped into "ops".
|
||||
expect(store["agent:main:work"]).toBeDefined();
|
||||
expect((store["agent:main:work"] as { sessionId: string }).sessionId).toBe("main-session");
|
||||
expect(store["agent:ops:work"]).toBeDefined();
|
||||
expect((store["agent:ops:work"] as { sessionId: string }).sessionId).toBe("ops-session");
|
||||
// The key must NOT have been merged into ops namespace
|
||||
expect(Object.keys(store).filter((k) => k.startsWith("agent:ops:")).length).toBe(1);
|
||||
});
|
||||
|
||||
it("lets the main agent claim bare main aliases in shared stores", async () => {
|
||||
const sharedStorePath = path.join(tmpDir, "shared-sessions.json");
|
||||
writeStore(sharedStorePath, {
|
||||
main: { sessionId: "main-session", updatedAt: 2000 },
|
||||
"agent:ops:work": { sessionId: "ops-session", updatedAt: 1000 },
|
||||
await withStateFixture(async ({ tmpDir, stateDir }) => {
|
||||
const sharedStorePath = path.join(tmpDir, "shared-sessions.json");
|
||||
writeStore(sharedStorePath, {
|
||||
main: { sessionId: "main-session", updatedAt: 2000 },
|
||||
"agent:ops:work": { sessionId: "ops-session", updatedAt: 1000 },
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work", store: sharedStorePath },
|
||||
agents: { list: [{ id: "main" }, { id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
const store = readStore(sharedStorePath);
|
||||
expect(store["agent:main:work"]).toBeDefined();
|
||||
expect((store["agent:main:work"] as { sessionId: string }).sessionId).toBe("main-session");
|
||||
expect(store.main).toBeUndefined();
|
||||
expect(store["agent:ops:work"]).toBeDefined();
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
session: { mainKey: "work", store: sharedStorePath },
|
||||
agents: { list: [{ id: "main" }, { id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
const store = readStore(sharedStorePath);
|
||||
expect(store["agent:main:work"]).toBeDefined();
|
||||
expect((store["agent:main:work"] as { sessionId: string }).sessionId).toBe("main-session");
|
||||
expect(store.main).toBeUndefined();
|
||||
expect(store["agent:ops:work"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("no-ops when default agentId is main and mainKey is main", async () => {
|
||||
const storePath = path.join(stateDir, "agents", "main", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
await withStateFixture(async ({ stateDir }) => {
|
||||
const storePath = path.join(stateDir, "agents", "main", "sessions", "sessions.json");
|
||||
writeStore(storePath, {
|
||||
"agent:main:main": { sessionId: "abc-123", updatedAt: 1000 },
|
||||
});
|
||||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes).toHaveLength(0);
|
||||
const store = readStore(storePath);
|
||||
expect(store["agent:main:main"]).toBeDefined();
|
||||
});
|
||||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
|
||||
const result = await migrateOrphanedSessionKeys({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: stateDir },
|
||||
});
|
||||
|
||||
expect(result.changes).toHaveLength(0);
|
||||
const store = readStore(storePath);
|
||||
expect(store["agent:main:main"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { EventEmitter } from "node:events";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runNodeWatchedPaths } from "../../scripts/run-node.mjs";
|
||||
import { runWatchMain } from "../../scripts/watch-node.mjs";
|
||||
import { bundledPluginFile } from "../../test/helpers/bundled-plugin-paths.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
|
||||
const VOICE_CALL_README = bundledPluginFile("voice-call", "README.md");
|
||||
const VOICE_CALL_MANIFEST = bundledPluginFile("voice-call", "openclaw.plugin.json");
|
||||
@@ -50,71 +50,74 @@ const createWatchHarness = () => {
|
||||
describe("watch-node script", () => {
|
||||
it("wires chokidar watch to run-node with watched source/config paths", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-watch-node-"));
|
||||
fs.mkdirSync(path.join(cwd, "src", "infra"), { recursive: true });
|
||||
fs.mkdirSync(path.join(cwd, "extensions", "voice-call"), { recursive: true });
|
||||
await withTempDir({ prefix: "openclaw-watch-node-" }, async (cwd) => {
|
||||
fs.mkdirSync(path.join(cwd, "src", "infra"), { recursive: true });
|
||||
fs.mkdirSync(path.join(cwd, "extensions", "voice-call"), { recursive: true });
|
||||
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
cwd,
|
||||
createWatcher,
|
||||
env: { PATH: "/usr/bin" },
|
||||
lockDisabled: true,
|
||||
now: () => 1700000000000,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
|
||||
expect(createWatcher).toHaveBeenCalledTimes(1);
|
||||
const firstWatcherCall = createWatcher.mock.calls[0];
|
||||
expect(firstWatcherCall).toBeDefined();
|
||||
const [watchPaths, watchOptions] = firstWatcherCall as unknown as [
|
||||
string[],
|
||||
{ ignoreInitial: boolean; ignored: (watchPath: string) => boolean },
|
||||
];
|
||||
expect(watchPaths).toEqual(runNodeWatchedPaths);
|
||||
expect(watchPaths).toContain("extensions");
|
||||
expect(watchPaths).toContain("tsdown.config.ts");
|
||||
expect(watchOptions.ignoreInitial).toBe(true);
|
||||
expect(watchOptions.ignored("src")).toBe(false);
|
||||
expect(watchOptions.ignored("src/infra")).toBe(false);
|
||||
expect(watchOptions.ignored("extensions")).toBe(false);
|
||||
expect(watchOptions.ignored("extensions/voice-call")).toBe(false);
|
||||
expect(watchOptions.ignored("extensions/voice-call/dist")).toBe(true);
|
||||
expect(watchOptions.ignored("extensions/voice-call/node_modules")).toBe(true);
|
||||
expect(watchOptions.ignored("extensions/voice-call/node_modules/chokidar/index.js")).toBe(true);
|
||||
expect(watchOptions.ignored("src/infra/watch-node.test.ts")).toBe(true);
|
||||
expect(watchOptions.ignored("src/infra/watch-node.test.tsx")).toBe(true);
|
||||
expect(watchOptions.ignored("src/infra/watch-node-test-helpers.ts")).toBe(true);
|
||||
expect(watchOptions.ignored(VOICE_CALL_README)).toBe(true);
|
||||
expect(watchOptions.ignored(VOICE_CALL_MANIFEST)).toBe(false);
|
||||
expect(watchOptions.ignored(VOICE_CALL_PACKAGE)).toBe(false);
|
||||
expect(watchOptions.ignored(VOICE_CALL_INDEX)).toBe(false);
|
||||
expect(watchOptions.ignored(VOICE_CALL_RUNTIME)).toBe(false);
|
||||
expect(watchOptions.ignored("src/infra/watch-node.ts")).toBe(false);
|
||||
expect(watchOptions.ignored("tsconfig.json")).toBe(false);
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
"/usr/local/bin/node",
|
||||
["scripts/run-node.mjs", "gateway", "--force"],
|
||||
expect.objectContaining({
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
env: expect.objectContaining({
|
||||
PATH: "/usr/bin",
|
||||
OPENCLAW_WATCH_MODE: "1",
|
||||
OPENCLAW_WATCH_SESSION: "1700000000000-4242",
|
||||
OPENCLAW_NO_RESPAWN: "1",
|
||||
OPENCLAW_WATCH_COMMAND: "gateway --force",
|
||||
createWatcher,
|
||||
env: { PATH: "/usr/bin" },
|
||||
lockDisabled: true,
|
||||
now: () => 1700000000000,
|
||||
process: fakeProcess,
|
||||
spawn,
|
||||
});
|
||||
|
||||
expect(createWatcher).toHaveBeenCalledTimes(1);
|
||||
const firstWatcherCall = createWatcher.mock.calls[0];
|
||||
expect(firstWatcherCall).toBeDefined();
|
||||
const [watchPaths, watchOptions] = firstWatcherCall as unknown as [
|
||||
string[],
|
||||
{ ignoreInitial: boolean; ignored: (watchPath: string) => boolean },
|
||||
];
|
||||
expect(watchPaths).toEqual(runNodeWatchedPaths);
|
||||
expect(watchPaths).toContain("extensions");
|
||||
expect(watchPaths).toContain("tsdown.config.ts");
|
||||
expect(watchOptions.ignoreInitial).toBe(true);
|
||||
expect(watchOptions.ignored("src")).toBe(false);
|
||||
expect(watchOptions.ignored("src/infra")).toBe(false);
|
||||
expect(watchOptions.ignored("extensions")).toBe(false);
|
||||
expect(watchOptions.ignored("extensions/voice-call")).toBe(false);
|
||||
expect(watchOptions.ignored("extensions/voice-call/dist")).toBe(true);
|
||||
expect(watchOptions.ignored("extensions/voice-call/node_modules")).toBe(true);
|
||||
expect(watchOptions.ignored("extensions/voice-call/node_modules/chokidar/index.js")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(watchOptions.ignored("src/infra/watch-node.test.ts")).toBe(true);
|
||||
expect(watchOptions.ignored("src/infra/watch-node.test.tsx")).toBe(true);
|
||||
expect(watchOptions.ignored("src/infra/watch-node-test-helpers.ts")).toBe(true);
|
||||
expect(watchOptions.ignored(VOICE_CALL_README)).toBe(true);
|
||||
expect(watchOptions.ignored(VOICE_CALL_MANIFEST)).toBe(false);
|
||||
expect(watchOptions.ignored(VOICE_CALL_PACKAGE)).toBe(false);
|
||||
expect(watchOptions.ignored(VOICE_CALL_INDEX)).toBe(false);
|
||||
expect(watchOptions.ignored(VOICE_CALL_RUNTIME)).toBe(false);
|
||||
expect(watchOptions.ignored("src/infra/watch-node.ts")).toBe(false);
|
||||
expect(watchOptions.ignored("tsconfig.json")).toBe(false);
|
||||
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
"/usr/local/bin/node",
|
||||
["scripts/run-node.mjs", "gateway", "--force"],
|
||||
expect.objectContaining({
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
env: expect.objectContaining({
|
||||
PATH: "/usr/bin",
|
||||
OPENCLAW_WATCH_MODE: "1",
|
||||
OPENCLAW_WATCH_SESSION: "1700000000000-4242",
|
||||
OPENCLAW_NO_RESPAWN: "1",
|
||||
OPENCLAW_WATCH_COMMAND: "gateway --force",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
fakeProcess.emit("SIGINT");
|
||||
const exitCode = await runPromise;
|
||||
expect(exitCode).toBe(130);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(watcher.close).toHaveBeenCalledTimes(1);
|
||||
);
|
||||
fakeProcess.emit("SIGINT");
|
||||
const exitCode = await runPromise;
|
||||
expect(exitCode).toBe(130);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(watcher.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("terminates child on SIGINT and returns shell interrupt code", async () => {
|
||||
@@ -345,63 +348,64 @@ describe("watch-node script", () => {
|
||||
|
||||
it("replaces an existing watcher lock holder before starting", async () => {
|
||||
const { child, spawn, watcher, createWatcher, fakeProcess } = createWatchHarness();
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-watch-node-lock-"));
|
||||
const lockPath = resolveTestWatchLockPath(cwd, ["gateway", "--force"]);
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
lockPath,
|
||||
`${JSON.stringify({
|
||||
pid: 2121,
|
||||
command: "gateway --force",
|
||||
createdAt: new Date(1_700_000_000_000).toISOString(),
|
||||
cwd,
|
||||
watchSession: "existing-session",
|
||||
})}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await withTempDir({ prefix: "openclaw-watch-node-lock-" }, async (cwd) => {
|
||||
const lockPath = resolveTestWatchLockPath(cwd, ["gateway", "--force"]);
|
||||
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
lockPath,
|
||||
`${JSON.stringify({
|
||||
pid: 2121,
|
||||
command: "gateway --force",
|
||||
createdAt: new Date(1_700_000_000_000).toISOString(),
|
||||
cwd,
|
||||
watchSession: "existing-session",
|
||||
})}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
let existingWatcherAlive = true;
|
||||
const signalProcess = vi.fn((pid: number, signal: NodeJS.Signals | 0) => {
|
||||
if (signal === 0) {
|
||||
if (pid === 2121 && existingWatcherAlive) {
|
||||
let existingWatcherAlive = true;
|
||||
const signalProcess = vi.fn((pid: number, signal: NodeJS.Signals | 0) => {
|
||||
if (signal === 0) {
|
||||
if (pid === 2121 && existingWatcherAlive) {
|
||||
return;
|
||||
}
|
||||
throw Object.assign(new Error("ESRCH"), { code: "ESRCH" });
|
||||
}
|
||||
if (pid === 2121 && signal === "SIGTERM") {
|
||||
existingWatcherAlive = false;
|
||||
return;
|
||||
}
|
||||
throw Object.assign(new Error("ESRCH"), { code: "ESRCH" });
|
||||
}
|
||||
if (pid === 2121 && signal === "SIGTERM") {
|
||||
existingWatcherAlive = false;
|
||||
return;
|
||||
}
|
||||
throw new Error(`unexpected signal ${signal} for pid ${pid}`);
|
||||
throw new Error(`unexpected signal ${signal} for pid ${pid}`);
|
||||
});
|
||||
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
cwd,
|
||||
now: () => 1_700_000_000_000,
|
||||
process: fakeProcess,
|
||||
signalProcess,
|
||||
sleep: async () => {},
|
||||
spawn,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(signalProcess).toHaveBeenCalledWith(2121, "SIGTERM");
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(fs.readFileSync(lockPath, "utf8"))).toMatchObject({
|
||||
pid: 4242,
|
||||
command: "gateway --force",
|
||||
watchSession: "1700000000000-4242",
|
||||
});
|
||||
|
||||
fakeProcess.emit("SIGINT");
|
||||
const exitCode = await runPromise;
|
||||
|
||||
expect(exitCode).toBe(130);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(fs.existsSync(lockPath)).toBe(false);
|
||||
expect(watcher.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const runPromise = runWatch({
|
||||
args: ["gateway", "--force"],
|
||||
createWatcher,
|
||||
cwd,
|
||||
now: () => 1_700_000_000_000,
|
||||
process: fakeProcess,
|
||||
signalProcess,
|
||||
sleep: async () => {},
|
||||
spawn,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(signalProcess).toHaveBeenCalledWith(2121, "SIGTERM");
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(fs.readFileSync(lockPath, "utf8"))).toMatchObject({
|
||||
pid: 4242,
|
||||
command: "gateway --force",
|
||||
watchSession: "1700000000000-4242",
|
||||
});
|
||||
|
||||
fakeProcess.emit("SIGINT");
|
||||
const exitCode = await runPromise;
|
||||
|
||||
expect(exitCode).toBe(130);
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(fs.existsSync(lockPath)).toBe(false);
|
||||
expect(watcher.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user