


























@@ -0,0 +1,144 @@
1+import { beforeEach, describe, expect, it, vi } from "vitest";
2+import type { RestartSentinelPayload } from "../../infra/restart-sentinel.js";
3+4+const isRestartEnabledMock = vi.fn(() => true);
5+const extractDeliveryInfoMock = vi.fn(() => ({
6+deliveryContext: {
7+channel: "slack",
8+to: "slack:C123",
9+accountId: "workspace-1",
10+},
11+threadId: "thread-42",
12+}));
13+const formatDoctorNonInteractiveHintMock = vi.fn(() => "Run: openclaw doctor --non-interactive");
14+const writeRestartSentinelMock = vi.fn(async (_payload: RestartSentinelPayload) => "/tmp/restart");
15+const scheduleGatewaySigusr1RestartMock = vi.fn(() => ({ scheduled: true, delayMs: 250 }));
16+17+vi.mock("../../config/commands.js", () => ({
18+isRestartEnabled: isRestartEnabledMock,
19+}));
20+21+vi.mock("../../config/sessions.js", () => ({
22+extractDeliveryInfo: extractDeliveryInfoMock,
23+}));
24+25+vi.mock("../../infra/restart-sentinel.js", async () => {
26+const actual = await vi.importActual<typeof import("../../infra/restart-sentinel.js")>(
27+"../../infra/restart-sentinel.js",
28+);
29+return {
30+ ...actual,
31+formatDoctorNonInteractiveHint: formatDoctorNonInteractiveHintMock,
32+writeRestartSentinel: writeRestartSentinelMock,
33+};
34+});
35+36+vi.mock("../../infra/restart.js", () => ({
37+scheduleGatewaySigusr1Restart: scheduleGatewaySigusr1RestartMock,
38+}));
39+40+vi.mock("../../logging/subsystem.js", () => ({
41+createSubsystemLogger: vi.fn(() => ({
42+info: vi.fn(),
43+})),
44+}));
45+46+vi.mock("./gateway.js", () => ({
47+callGatewayTool: vi.fn(),
48+readGatewayCallOptions: vi.fn(() => ({})),
49+}));
50+51+describe("gateway tool restart continuation", () => {
52+beforeEach(() => {
53+isRestartEnabledMock.mockReset();
54+isRestartEnabledMock.mockReturnValue(true);
55+extractDeliveryInfoMock.mockReset();
56+extractDeliveryInfoMock.mockReturnValue({
57+deliveryContext: {
58+channel: "slack",
59+to: "slack:C123",
60+accountId: "workspace-1",
61+},
62+threadId: "thread-42",
63+});
64+formatDoctorNonInteractiveHintMock.mockReset();
65+formatDoctorNonInteractiveHintMock.mockReturnValue("Run: openclaw doctor --non-interactive");
66+writeRestartSentinelMock.mockReset();
67+writeRestartSentinelMock.mockResolvedValue("/tmp/restart");
68+scheduleGatewaySigusr1RestartMock.mockReset();
69+scheduleGatewaySigusr1RestartMock.mockReturnValue({ scheduled: true, delayMs: 250 });
70+});
71+72+it("uses a flat enum for continuationKind in the tool schema", async () => {
73+const { createGatewayTool } = await import("./gateway-tool.js");
74+const tool = createGatewayTool();
75+const continuationKind = (
76+tool.parameters as {
77+properties?: {
78+continuationKind?: {
79+type?: string;
80+enum?: string[];
81+anyOf?: unknown[];
82+};
83+};
84+}
85+).properties?.continuationKind;
86+87+expect(continuationKind).toEqual(
88+expect.objectContaining({
89+type: "string",
90+enum: ["systemEvent", "agentTurn"],
91+}),
92+);
93+expect(continuationKind).not.toHaveProperty("anyOf");
94+});
95+96+it("instructs agents to use continuationMessage when a restart still needs a reply", async () => {
97+const { createGatewayTool } = await import("./gateway-tool.js");
98+const tool = createGatewayTool();
99+100+expect(tool.description).toContain("still owe the user a reply");
101+expect(tool.description).toContain("continuationMessage");
102+expect(tool.description).toContain("do not write restart sentinel files directly");
103+});
104+105+it("writes an agentTurn continuation into the restart sentinel", async () => {
106+const { createGatewayTool } = await import("./gateway-tool.js");
107+const tool = createGatewayTool({
108+agentSessionKey: "agent:main:main",
109+config: {},
110+});
111+112+const result = await tool.execute?.("tool-call-1", {
113+action: "restart",
114+delayMs: 250,
115+reason: "continue after reboot",
116+note: "Gateway restarting now",
117+continuationMessage: "Reply with exactly: Yay! I did it!",
118+});
119+120+expect(writeRestartSentinelMock).toHaveBeenCalledWith(
121+expect.objectContaining({
122+kind: "restart",
123+status: "ok",
124+sessionKey: "agent:main:main",
125+deliveryContext: {
126+channel: "slack",
127+to: "slack:C123",
128+accountId: "workspace-1",
129+},
130+threadId: "thread-42",
131+message: "Gateway restarting now",
132+continuation: {
133+kind: "agentTurn",
134+message: "Reply with exactly: Yay! I did it!",
135+},
136+}),
137+);
138+expect(scheduleGatewaySigusr1RestartMock).toHaveBeenCalledWith({
139+delayMs: 250,
140+reason: "continue after reboot",
141+});
142+expect(result?.details).toEqual({ scheduled: true, delayMs: 250 });
143+});
144+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。