























@@ -5,9 +5,9 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
55import type { CommandContext } from "../auto-reply/reply/commands-types.js";
66import type { OpenClawConfig } from "../config/types.openclaw.js";
77import type { RuntimeEnv } from "../runtime.js";
8+import { withEnvAsync } from "../test-utils/env.js";
89import { extractCrestodianRescueMessage, runCrestodianRescueMessage } from "./rescue-message.js";
91010-const originalStateDir = process.env.OPENCLAW_STATE_DIR;
1111let tempRoot = "";
1212let tempDirId = 0;
1313@@ -111,6 +111,14 @@ async function makeStateDir(prefix: string): Promise<string> {
111111return dir;
112112}
113113114+async function withRescueStateDir(
115+prefix: string,
116+run: (stateDir: string) => Promise<void>,
117+): Promise<void> {
118+const stateDir = await makeStateDir(prefix);
119+await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => await run(stateDir));
120+}
121+114122function commandContext(overrides: Partial<CommandContext> = {}): CommandContext {
115123return {
116124surface: "whatsapp",
@@ -160,14 +168,6 @@ describe("Crestodian rescue message", () => {
160168mockConfig.reset();
161169});
162170163-afterEach(() => {
164-if (originalStateDir === undefined) {
165-delete process.env.OPENCLAW_STATE_DIR;
166-} else {
167-process.env.OPENCLAW_STATE_DIR = originalStateDir;
168-}
169-});
170-171171afterAll(async () => {
172172if (tempRoot) {
173173await fs.rm(tempRoot, { recursive: true, force: true });
@@ -248,148 +248,147 @@ describe("Crestodian rescue message", () => {
248248});
249249250250it("queues and applies persistent writes through conversational approval", async () => {
251-const tempDir = await makeStateDir("models-");
252-vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
253-254-const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
255-await expect(runRescue("/crestodian set default model openai/gpt-5.2", cfg)).resolves.toContain(
256-"Reply /crestodian yes to apply",
257-);
258-await expect(runRescue("/crestodian yes", cfg)).resolves.toContain(
259-"Default model: openai/gpt-5.2",
260-);
251+await withRescueStateDir("models-", async (tempDir) => {
252+const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
253+await expect(
254+runRescue("/crestodian set default model openai/gpt-5.2", cfg),
255+).resolves.toContain("Reply /crestodian yes to apply");
256+await expect(runRescue("/crestodian yes", cfg)).resolves.toContain(
257+"Default model: openai/gpt-5.2",
258+);
261259262-const currentConfig = mockConfig.currentConfig() as {
263-agents?: { defaults?: { model?: { primary?: string } } };
264-};
265-expect(currentConfig.agents?.defaults?.model?.primary).toBe("openai/gpt-5.2");
266-const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
267-const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim()) as {
268-details?: { rescue?: boolean; channel?: string; senderId?: string };
269-};
270-expect(audit.details?.rescue).toBe(true);
271-expect(audit.details?.channel).toBe("whatsapp");
272-expect(audit.details?.senderId).toBe("user:owner");
260+const currentConfig = mockConfig.currentConfig() as {
261+agents?: { defaults?: { model?: { primary?: string } } };
262+};
263+expect(currentConfig.agents?.defaults?.model?.primary).toBe("openai/gpt-5.2");
264+const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
265+const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim()) as {
266+details?: { rescue?: boolean; channel?: string; senderId?: string };
267+};
268+expect(audit.details?.rescue).toBe(true);
269+expect(audit.details?.channel).toBe("whatsapp");
270+expect(audit.details?.senderId).toBe("user:owner");
271+});
273272});
274273275274it("queues and applies gateway restart through conversational approval", async () => {
276-const tempDir = await makeStateDir("gateway-");
277-vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
278-const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
279-const deps = { runGatewayRestart: vi.fn(async () => {}) };
275+await withRescueStateDir("gateway-", async (tempDir) => {
276+const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
277+const deps = { runGatewayRestart: vi.fn(async () => {}) };
280278281-await expect(
282-runRescue("/crestodian restart gateway", cfg, commandContext(), deps),
283-).resolves.toBe("Plan: restart the Gateway. Reply /crestodian yes to apply.");
284-await expect(runRescue("/crestodian yes", cfg, commandContext(), deps)).resolves.toContain(
285-"[crestodian] done: gateway.restart",
286-);
279+ await expect(
280+ runRescue("/crestodian restart gateway", cfg, commandContext(), deps),
281+ ).resolves.toBe("Plan: restart the Gateway. Reply /crestodian yes to apply.");
282+ await expect(runRescue("/crestodian yes", cfg, commandContext(), deps)).resolves.toContain(
283+ "[crestodian] done: gateway.restart",
284+ );
287285288-expect(deps.runGatewayRestart).toHaveBeenCalledTimes(1);
289-const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
290-const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim()) as {
291-operation?: string;
292-details?: { rescue?: boolean; channel?: string; senderId?: string };
293-};
294-expect(audit.operation).toBe("gateway.restart");
295-expect(audit.details?.rescue).toBe(true);
296-expect(audit.details?.channel).toBe("whatsapp");
297-expect(audit.details?.senderId).toBe("user:owner");
286+expect(deps.runGatewayRestart).toHaveBeenCalledTimes(1);
287+const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
288+const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim()) as {
289+operation?: string;
290+details?: { rescue?: boolean; channel?: string; senderId?: string };
291+};
292+expect(audit.operation).toBe("gateway.restart");
293+expect(audit.details?.rescue).toBe(true);
294+expect(audit.details?.channel).toBe("whatsapp");
295+expect(audit.details?.senderId).toBe("user:owner");
296+});
298297});
299298300299it("does not queue persistent rescue approval when expiry would exceed the Date range", async () => {
301-const tempDir = await makeStateDir("overflow-expiry-");
302-vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
303-vi.useFakeTimers();
304-vi.setSystemTime(new Date(8_640_000_000_000_000));
305-try {
306- const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
307-308-await expect(
309-runRescue("/crestodian restart gateway", cfg, commandContext()),
310- ).resolves.toContain("expiry clock is invalid");
311-312-await expect(fs.readdir(path.join(tempDir, "crestodian", "rescue-pending"))).rejects.toThrow(
313-/ENOENT/,
314-);
315-} finally {
316-vi.useRealTimers();
317-}
300+await withRescueStateDir("overflow-expiry-", async (tempDir) => {
301+ vi.useFakeTimers();
302+ vi.setSystemTime(new Date(8_640_000_000_000_000));
303+ try {
304+ const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
305+306+ await expect(
307+ runRescue("/crestodian restart gateway", cfg, commandContext()),
308+).resolves.toContain("expiry clock is invalid");
309+310+ await expect(
311+ fs.readdir(path.join(tempDir, "crestodian", "rescue-pending")),
312+).rejects.toThrow(/ENOENT/);
313+} finally {
314+ vi.useRealTimers();
315+}
316+});
318317});
319318320319it("rejects pending rescue approvals with invalid persisted expiry", async () => {
321-const tempDir = await makeStateDir("invalid-expiry-");
322-vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
323-const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
324-const deps = { runGatewayRestart: vi.fn(async () => {}) };
325-326-await expect(
327-runRescue("/crestodian restart gateway", cfg, commandContext(), deps),
328-).resolves.toContain("Reply /crestodian yes to apply");
329-const pendingDir = path.join(tempDir, "crestodian", "rescue-pending");
330-const [pendingFile] = await fs.readdir(pendingDir);
331-if (!pendingFile) {
332-throw new Error("expected pending rescue file");
333-}
334-const pendingPath = path.join(pendingDir, pendingFile);
335-const pending = JSON.parse(await fs.readFile(pendingPath, "utf8")) as { expiresAt?: string };
336-pending.expiresAt = "not-a-date";
337-await fs.writeFile(pendingPath, `${JSON.stringify(pending, null, 2)}\n`, "utf8");
320+await withRescueStateDir("invalid-expiry-", async (tempDir) => {
321+const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
322+const deps = { runGatewayRestart: vi.fn(async () => {}) };
338323339-await expect(runRescue("/crestodian yes", cfg, commandContext(), deps)).resolves.toBe(
340-"No pending Crestodian rescue change is waiting for approval.",
341-);
342-expect(deps.runGatewayRestart).not.toHaveBeenCalled();
343-await expect(fs.stat(pendingPath)).rejects.toThrow(/ENOENT/);
324+await expect(
325+runRescue("/crestodian restart gateway", cfg, commandContext(), deps),
326+).resolves.toContain("Reply /crestodian yes to apply");
327+const pendingDir = path.join(tempDir, "crestodian", "rescue-pending");
328+const [pendingFile] = await fs.readdir(pendingDir);
329+if (!pendingFile) {
330+throw new Error("expected pending rescue file");
331+}
332+const pendingPath = path.join(pendingDir, pendingFile);
333+const pending = JSON.parse(await fs.readFile(pendingPath, "utf8")) as { expiresAt?: string };
334+pending.expiresAt = "not-a-date";
335+await fs.writeFile(pendingPath, `${JSON.stringify(pending, null, 2)}\n`, "utf8");
336+337+await expect(runRescue("/crestodian yes", cfg, commandContext(), deps)).resolves.toBe(
338+"No pending Crestodian rescue change is waiting for approval.",
339+);
340+expect(deps.runGatewayRestart).not.toHaveBeenCalled();
341+await expect(fs.stat(pendingPath)).rejects.toThrow(/ENOENT/);
342+});
344343});
345344346345it("queues and applies agent creation through conversational approval", async () => {
347-const tempDir = await makeStateDir("agent-");
348-vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
349-const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
350-const deps = { runAgentsAdd: vi.fn(async () => {}) };
346+await withRescueStateDir("agent-", async (tempDir) => {
347+const cfg: OpenClawConfig = { crestodian: { rescue: { enabled: true } } };
348+const deps = { runAgentsAdd: vi.fn(async () => {}) };
351349352-await expect(
353-runRescue("/crestodian create agent work workspace /tmp/work", cfg, commandContext(), deps),
354-).resolves.toBe(
355-"Plan: create agent work with workspace /tmp/work. Reply /crestodian yes to apply.",
356-);
357-await expect(runRescue("/crestodian yes", cfg, commandContext(), deps)).resolves.toContain(
358-"[crestodian] done: agents.create",
359-);
350+ await expect(
351+ runRescue("/crestodian create agent work workspace /tmp/work", cfg, commandContext(), deps),
352+ ).resolves.toBe(
353+ "Plan: create agent work with workspace /tmp/work. Reply /crestodian yes to apply.",
354+ );
355+ await expect(runRescue("/crestodian yes", cfg, commandContext(), deps)).resolves.toContain(
356+ "[crestodian] done: agents.create",
357+ );
360358361-expect(deps.runAgentsAdd).toHaveBeenCalledTimes(1);
362-const [agentParams, agentRuntime, agentOptions] = requireFirstMockCall(
363-deps.runAgentsAdd,
364-"agents add",
365-) as unknown as [
366-{ name: string; workspace: string; nonInteractive: boolean },
367-object,
368-{ hasFlags: boolean },
369-];
370-expect(agentParams).toEqual({
371-name: "work",
372-workspace: "/tmp/work",
373-nonInteractive: true,
374-});
375-expect(agentRuntime).toBeTypeOf("object");
376-expect(agentOptions).toEqual({ hasFlags: true });
377-const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
378-const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim()) as {
379-operation?: string;
380-details?: {
381-rescue?: boolean;
382-channel?: string;
383-senderId?: string;
384-agentId?: string;
385-workspace?: string;
359+expect(deps.runAgentsAdd).toHaveBeenCalledTimes(1);
360+const [agentParams, agentRuntime, agentOptions] = requireFirstMockCall(
361+deps.runAgentsAdd,
362+"agents add",
363+) as unknown as [
364+{ name: string; workspace: string; nonInteractive: boolean },
365+object,
366+{ hasFlags: boolean },
367+];
368+expect(agentParams).toEqual({
369+name: "work",
370+workspace: "/tmp/work",
371+nonInteractive: true,
372+});
373+expect(agentRuntime).toBeTypeOf("object");
374+expect(agentOptions).toEqual({ hasFlags: true });
375+const auditPath = path.join(tempDir, "audit", "crestodian.jsonl");
376+const audit = JSON.parse((await fs.readFile(auditPath, "utf8")).trim()) as {
377+operation?: string;
378+details?: {
379+rescue?: boolean;
380+channel?: string;
381+senderId?: string;
382+agentId?: string;
383+workspace?: string;
384+};
386385};
387-};
388-expect(audit.operation).toBe("agents.create");
389-expect(audit.details?.rescue).toBe(true);
390-expect(audit.details?.channel).toBe("whatsapp");
391-expect(audit.details?.senderId).toBe("user:owner");
392-expect(audit.details?.agentId).toBe("work");
393-expect(audit.details?.workspace).toBe("/tmp/work");
386+ expect(audit.operation).toBe("agents.create");
387+ expect(audit.details?.rescue).toBe(true);
388+ expect(audit.details?.channel).toBe("whatsapp");
389+ expect(audit.details?.senderId).toBe("user:owner");
390+ expect(audit.details?.agentId).toBe("work");
391+ expect(audit.details?.workspace).toBe("/tmp/work");
392+});
394393});
395394});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。