mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:54:30 +00:00
test(gateway): dedupe boot workspace setup and cover boot failures
This commit is contained in:
@@ -15,6 +15,11 @@ const { resolveStorePath } = await import("../config/sessions/paths.js");
|
|||||||
const { loadSessionStore, saveSessionStore } = await import("../config/sessions/store.js");
|
const { loadSessionStore, saveSessionStore } = await import("../config/sessions/store.js");
|
||||||
|
|
||||||
describe("runBootOnce", () => {
|
describe("runBootOnce", () => {
|
||||||
|
type BootWorkspaceOptions = {
|
||||||
|
bootAsDirectory?: boolean;
|
||||||
|
bootContent?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const resolveMainStore = (
|
const resolveMainStore = (
|
||||||
cfg: {
|
cfg: {
|
||||||
session?: { store?: string; scope?: SessionScope; mainKey?: string };
|
session?: { store?: string; scope?: SessionScope; mainKey?: string };
|
||||||
@@ -42,6 +47,24 @@ describe("runBootOnce", () => {
|
|||||||
sendMessageIMessage: vi.fn(),
|
sendMessageIMessage: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const withBootWorkspace = async (
|
||||||
|
options: BootWorkspaceOptions,
|
||||||
|
run: (workspaceDir: string) => Promise<void>,
|
||||||
|
) => {
|
||||||
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
||||||
|
try {
|
||||||
|
const bootPath = path.join(workspaceDir, "BOOT.md");
|
||||||
|
if (options.bootAsDirectory) {
|
||||||
|
await fs.mkdir(bootPath, { recursive: true });
|
||||||
|
} else if (typeof options.bootContent === "string") {
|
||||||
|
await fs.writeFile(bootPath, options.bootContent, "utf-8");
|
||||||
|
}
|
||||||
|
await run(workspaceDir);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const mockAgentUpdatesMainSession = (storePath: string, sessionKey: string) => {
|
const mockAgentUpdatesMainSession = (storePath: string, sessionKey: string) => {
|
||||||
agentCommand.mockImplementation(async (opts: { sessionId?: string }) => {
|
agentCommand.mockImplementation(async (opts: { sessionId?: string }) => {
|
||||||
const current = loadSessionStore(storePath, { skipCache: true });
|
const current = loadSessionStore(storePath, { skipCache: true });
|
||||||
@@ -54,166 +77,173 @@ describe("runBootOnce", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("skips when BOOT.md is missing", async () => {
|
it("skips when BOOT.md is missing", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
await withBootWorkspace({}, async (workspaceDir) => {
|
||||||
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
status: "skipped",
|
status: "skipped",
|
||||||
reason: "missing",
|
reason: "missing",
|
||||||
|
});
|
||||||
|
expect(agentCommand).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns failed when BOOT.md cannot be read", async () => {
|
||||||
|
await withBootWorkspace({ bootAsDirectory: true }, async (workspaceDir) => {
|
||||||
|
const result = await runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir });
|
||||||
|
expect(result.status).toBe("failed");
|
||||||
|
if (result.status === "failed") {
|
||||||
|
expect(result.reason.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
expect(agentCommand).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
expect(agentCommand).not.toHaveBeenCalled();
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ title: "empty", content: " \n", reason: "empty" as const },
|
{ title: "empty", content: " \n", reason: "empty" as const },
|
||||||
{ title: "whitespace-only", content: "\n\t ", reason: "empty" as const },
|
{ title: "whitespace-only", content: "\n\t ", reason: "empty" as const },
|
||||||
])("skips when BOOT.md is $title", async ({ content, reason }) => {
|
])("skips when BOOT.md is $title", async ({ content, reason }) => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
await withBootWorkspace({ bootContent: content }, async (workspaceDir) => {
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
status: "skipped",
|
||||||
status: "skipped",
|
reason,
|
||||||
reason,
|
});
|
||||||
|
expect(agentCommand).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
expect(agentCommand).not.toHaveBeenCalled();
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("runs agent command when BOOT.md exists", async () => {
|
it("runs agent command when BOOT.md exists", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
|
||||||
const content = "Say hello when you wake up.";
|
const content = "Say hello when you wake up.";
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
await withBootWorkspace({ bootContent: content }, async (workspaceDir) => {
|
||||||
|
agentCommand.mockResolvedValue(undefined);
|
||||||
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
|
status: "ran",
|
||||||
|
});
|
||||||
|
|
||||||
agentCommand.mockResolvedValue(undefined);
|
expect(agentCommand).toHaveBeenCalledTimes(1);
|
||||||
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
const call = agentCommand.mock.calls[0]?.[0];
|
||||||
status: "ran",
|
expect(call).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
deliver: false,
|
||||||
|
sessionKey: resolveMainSessionKey({}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(call?.message).toContain("BOOT.md:");
|
||||||
|
expect(call?.message).toContain(content);
|
||||||
|
expect(call?.message).toContain("NO_REPLY");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(agentCommand).toHaveBeenCalledTimes(1);
|
it("returns failed when agent command throws", async () => {
|
||||||
const call = agentCommand.mock.calls[0]?.[0];
|
await withBootWorkspace({ bootContent: "Wake up and report." }, async (workspaceDir) => {
|
||||||
expect(call).toEqual(
|
agentCommand.mockRejectedValue(new Error("boom"));
|
||||||
expect.objectContaining({
|
await expect(runBootOnce({ cfg: {}, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
deliver: false,
|
status: "failed",
|
||||||
sessionKey: resolveMainSessionKey({}),
|
reason: expect.stringContaining("agent run failed: boom"),
|
||||||
}),
|
});
|
||||||
);
|
expect(agentCommand).toHaveBeenCalledTimes(1);
|
||||||
expect(call?.message).toContain("BOOT.md:");
|
});
|
||||||
expect(call?.message).toContain(content);
|
|
||||||
expect(call?.message).toContain("NO_REPLY");
|
|
||||||
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses per-agent session key when agentId is provided", async () => {
|
it("uses per-agent session key when agentId is provided", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
await withBootWorkspace({ bootContent: "Check status." }, async (workspaceDir) => {
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), "Check status.", "utf-8");
|
agentCommand.mockResolvedValue(undefined);
|
||||||
|
const cfg = {};
|
||||||
|
const agentId = "ops";
|
||||||
|
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir, agentId })).resolves.toEqual({
|
||||||
|
status: "ran",
|
||||||
|
});
|
||||||
|
|
||||||
agentCommand.mockResolvedValue(undefined);
|
expect(agentCommand).toHaveBeenCalledTimes(1);
|
||||||
const cfg = {};
|
const perAgentCall = agentCommand.mock.calls[0]?.[0];
|
||||||
const agentId = "ops";
|
expect(perAgentCall?.sessionKey).toBe(resolveAgentMainSessionKey({ cfg, agentId }));
|
||||||
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir, agentId })).resolves.toEqual({
|
|
||||||
status: "ran",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(agentCommand).toHaveBeenCalledTimes(1);
|
|
||||||
const perAgentCall = agentCommand.mock.calls[0]?.[0];
|
|
||||||
expect(perAgentCall?.sessionKey).toBe(resolveAgentMainSessionKey({ cfg, agentId }));
|
|
||||||
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("generates new session ID when no existing session exists", async () => {
|
it("generates new session ID when no existing session exists", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
|
||||||
const content = "Say hello when you wake up.";
|
const content = "Say hello when you wake up.";
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
await withBootWorkspace({ bootContent: content }, async (workspaceDir) => {
|
||||||
|
agentCommand.mockResolvedValue(undefined);
|
||||||
|
const cfg = {};
|
||||||
|
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
|
status: "ran",
|
||||||
|
});
|
||||||
|
|
||||||
agentCommand.mockResolvedValue(undefined);
|
expect(agentCommand).toHaveBeenCalledTimes(1);
|
||||||
const cfg = {};
|
const call = agentCommand.mock.calls[0]?.[0];
|
||||||
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
|
||||||
status: "ran",
|
// Verify a boot-style session ID was generated (format: boot-YYYY-MM-DD_HH-MM-SS-xxx-xxxxxxxx)
|
||||||
|
expect(call?.sessionId).toMatch(
|
||||||
|
/^boot-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{8}$/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(agentCommand).toHaveBeenCalledTimes(1);
|
|
||||||
const call = agentCommand.mock.calls[0]?.[0];
|
|
||||||
|
|
||||||
// Verify a boot-style session ID was generated (format: boot-YYYY-MM-DD_HH-MM-SS-xxx-xxxxxxxx)
|
|
||||||
expect(call?.sessionId).toMatch(/^boot-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{8}$/);
|
|
||||||
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses a fresh boot session ID even when main session mapping already exists", async () => {
|
it("uses a fresh boot session ID even when main session mapping already exists", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
|
||||||
const content = "Say hello when you wake up.";
|
const content = "Say hello when you wake up.";
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
await withBootWorkspace({ bootContent: content }, async (workspaceDir) => {
|
||||||
|
const cfg = {};
|
||||||
|
const { sessionKey, storePath } = resolveMainStore(cfg);
|
||||||
|
const existingSessionId = "main-session-abc123";
|
||||||
|
|
||||||
const cfg = {};
|
await saveSessionStore(storePath, {
|
||||||
const { sessionKey, storePath } = resolveMainStore(cfg);
|
[sessionKey]: {
|
||||||
const existingSessionId = "main-session-abc123";
|
sessionId: existingSessionId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await saveSessionStore(storePath, {
|
agentCommand.mockResolvedValue(undefined);
|
||||||
[sessionKey]: {
|
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
sessionId: existingSessionId,
|
status: "ran",
|
||||||
updatedAt: Date.now(),
|
});
|
||||||
},
|
|
||||||
|
expect(agentCommand).toHaveBeenCalledTimes(1);
|
||||||
|
const call = agentCommand.mock.calls[0]?.[0];
|
||||||
|
|
||||||
|
expect(call?.sessionId).not.toBe(existingSessionId);
|
||||||
|
expect(call?.sessionId).toMatch(
|
||||||
|
/^boot-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{8}$/,
|
||||||
|
);
|
||||||
|
expect(call?.sessionKey).toBe(sessionKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
agentCommand.mockResolvedValue(undefined);
|
|
||||||
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
|
||||||
status: "ran",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(agentCommand).toHaveBeenCalledTimes(1);
|
|
||||||
const call = agentCommand.mock.calls[0]?.[0];
|
|
||||||
|
|
||||||
expect(call?.sessionId).not.toBe(existingSessionId);
|
|
||||||
expect(call?.sessionId).toMatch(/^boot-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{8}$/);
|
|
||||||
expect(call?.sessionKey).toBe(sessionKey);
|
|
||||||
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("restores the original main session mapping after the boot run", async () => {
|
it("restores the original main session mapping after the boot run", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
|
||||||
const content = "Check if the system is healthy.";
|
const content = "Check if the system is healthy.";
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), content, "utf-8");
|
await withBootWorkspace({ bootContent: content }, async (workspaceDir) => {
|
||||||
|
const cfg = {};
|
||||||
|
const { sessionKey, storePath } = resolveMainStore(cfg);
|
||||||
|
const existingSessionId = "main-session-xyz789";
|
||||||
|
|
||||||
const cfg = {};
|
await saveSessionStore(storePath, {
|
||||||
const { sessionKey, storePath } = resolveMainStore(cfg);
|
[sessionKey]: {
|
||||||
const existingSessionId = "main-session-xyz789";
|
sessionId: existingSessionId,
|
||||||
|
updatedAt: Date.now() - 60_000, // 1 minute ago
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await saveSessionStore(storePath, {
|
mockAgentUpdatesMainSession(storePath, sessionKey);
|
||||||
[sessionKey]: {
|
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
sessionId: existingSessionId,
|
status: "ran",
|
||||||
updatedAt: Date.now() - 60_000, // 1 minute ago
|
});
|
||||||
},
|
|
||||||
|
const restored = loadSessionStore(storePath, { skipCache: true });
|
||||||
|
expect(restored[sessionKey]?.sessionId).toBe(existingSessionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
mockAgentUpdatesMainSession(storePath, sessionKey);
|
|
||||||
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
|
||||||
status: "ran",
|
|
||||||
});
|
|
||||||
|
|
||||||
const restored = loadSessionStore(storePath, { skipCache: true });
|
|
||||||
expect(restored[sessionKey]?.sessionId).toBe(existingSessionId);
|
|
||||||
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes a boot-created main-session mapping when none existed before", async () => {
|
it("removes a boot-created main-session mapping when none existed before", async () => {
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-boot-"));
|
await withBootWorkspace({ bootContent: "health check" }, async (workspaceDir) => {
|
||||||
await fs.writeFile(path.join(workspaceDir, "BOOT.md"), "health check", "utf-8");
|
const cfg = {};
|
||||||
|
const { sessionKey, storePath } = resolveMainStore(cfg);
|
||||||
|
|
||||||
const cfg = {};
|
mockAgentUpdatesMainSession(storePath, sessionKey);
|
||||||
const { sessionKey, storePath } = resolveMainStore(cfg);
|
|
||||||
|
|
||||||
mockAgentUpdatesMainSession(storePath, sessionKey);
|
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
||||||
|
status: "ran",
|
||||||
|
});
|
||||||
|
|
||||||
await expect(runBootOnce({ cfg, deps: makeDeps(), workspaceDir })).resolves.toEqual({
|
const restored = loadSessionStore(storePath, { skipCache: true });
|
||||||
status: "ran",
|
expect(restored[sessionKey]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
const restored = loadSessionStore(storePath, { skipCache: true });
|
|
||||||
expect(restored[sessionKey]).toBeUndefined();
|
|
||||||
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user