




























@@ -3,16 +3,26 @@ import fs from "node:fs";
33import os from "node:os";
44import path from "node:path";
55import { afterEach, describe, expect, it } from "vitest";
6+import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
7+import {
8+closeOpenClawStateDatabaseForTest,
9+openOpenClawStateDatabase,
10+} from "../state/openclaw-state-db.js";
11+import {
12+executeSqliteQuerySync,
13+executeSqliteQueryTakeFirstSync,
14+getNodeSqliteKysely,
15+} from "./kysely-sync.js";
616import {
717formatGatewayRestartHandoffDiagnostic,
8-GATEWAY_SUPERVISOR_RESTART_HANDOFF_FILENAME,
918GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
1019readGatewayRestartHandoffSync,
1120writeGatewayRestartHandoffSync,
1221} from "./restart-handoff.js";
1322import type { GatewayRestartHandoff } from "./restart-handoff.js";
14231524const tempDirs: string[] = [];
25+type GatewayRestartHandoffDatabase = Pick<OpenClawStateKyselyDatabase, "gateway_restart_handoff">;
16261727function createHandoffEnv(): NodeJS.ProcessEnv {
1828const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-restart-handoff-"));
@@ -23,8 +33,77 @@ function createHandoffEnv(): NodeJS.ProcessEnv {
2333};
2434}
253526-function handoffPath(env: NodeJS.ProcessEnv): string {
27-return path.join(env.OPENCLAW_STATE_DIR ?? "", GATEWAY_SUPERVISOR_RESTART_HANDOFF_FILENAME);
36+function legacyHandoffPath(env: NodeJS.ProcessEnv): string {
37+return path.join(env.OPENCLAW_STATE_DIR ?? "", "gateway-supervisor-restart-handoff.json");
38+}
39+40+function readHandoffRow(env: NodeJS.ProcessEnv) {
41+const { db } = openOpenClawStateDatabase({ env });
42+const stateDb = getNodeSqliteKysely<GatewayRestartHandoffDatabase>(db);
43+return executeSqliteQueryTakeFirstSync(
44+db,
45+stateDb
46+.selectFrom("gateway_restart_handoff")
47+.select([
48+"handoff_key",
49+"kind",
50+"version",
51+"intent_id",
52+"pid",
53+"process_instance_id",
54+"created_at",
55+"expires_at",
56+"reason",
57+"restart_trace_started_at",
58+"restart_trace_last_at",
59+"source",
60+"restart_kind",
61+"supervisor_mode",
62+])
63+.where("handoff_key", "=", "current"),
64+);
65+}
66+67+function insertHandoffRow(
68+env: NodeJS.ProcessEnv,
69+values: {
70+kind?: string;
71+version?: number;
72+intentId?: string;
73+pid?: number;
74+createdAt?: number;
75+expiresAt?: number;
76+reason?: string | null;
77+source?: string;
78+restartKind?: string;
79+supervisorMode?: string;
80+restartTraceStartedAt?: number | null;
81+restartTraceLastAt?: number | null;
82+},
83+) {
84+const { db } = openOpenClawStateDatabase({ env });
85+const stateDb = getNodeSqliteKysely<GatewayRestartHandoffDatabase>(db);
86+const now = Date.now();
87+executeSqliteQuerySync(
88+db,
89+stateDb.insertInto("gateway_restart_handoff").values({
90+handoff_key: "current",
91+kind: values.kind ?? GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
92+version: values.version ?? 1,
93+intent_id: values.intentId ?? "intent-1",
94+pid: values.pid ?? 111,
95+process_instance_id: null,
96+created_at: values.createdAt ?? 1_000,
97+expires_at: values.expiresAt ?? 61_000,
98+reason: values.reason ?? null,
99+restart_trace_started_at: values.restartTraceStartedAt ?? null,
100+restart_trace_last_at: values.restartTraceLastAt ?? null,
101+source: values.source ?? "plugin-change",
102+restart_kind: values.restartKind ?? "full-process",
103+supervisor_mode: values.supervisorMode ?? "external",
104+updated_at_ms: now,
105+}),
106+);
28107}
2910830109function expectWrittenHandoff(
@@ -39,6 +118,7 @@ function expectWrittenHandoff(
3911840119describe("gateway restart handoff", () => {
41120afterEach(() => {
121+closeOpenClawStateDatabaseForTest();
42122for (const dir of tempDirs.splice(0)) {
43123fs.rmSync(dir, { force: true, recursive: true });
44124}
@@ -67,7 +147,16 @@ describe("gateway restart handoff", () => {
67147expect(handoff.supervisorMode).toBe("launchd");
68148expect(handoff.createdAt).toBe(1_000);
69149expect(handoff.expiresAt).toBe(61_000);
70-expect(fs.statSync(handoffPath(env)).mode & 0o777).toBe(0o600);
150+expect(readHandoffRow(env)).toMatchObject({
151+handoff_key: "current",
152+kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
153+pid: 12_345,
154+reason: "plugin source changed",
155+source: "plugin-change",
156+restart_kind: "full-process",
157+supervisor_mode: "launchd",
158+});
159+expect(fs.existsSync(legacyHandoffPath(env))).toBe(false);
71160const persisted = readGatewayRestartHandoffSync(env, 1_500);
72161expect(persisted?.pid).toBe(12_345);
73162expect(persisted?.reason).toBe("plugin source changed");
@@ -126,27 +215,12 @@ describe("gateway restart handoff", () => {
126215it("rejects malformed handoff payloads", () => {
127216const env = createHandoffEnv();
128217129-fs.writeFileSync(
130-handoffPath(env),
131-`${JSON.stringify({
132- kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
133- version: 1,
134- intentId: "bad",
135- pid: 111,
136- createdAt: 1_000,
137- expiresAt: 61_000,
138- reason: 123,
139- source: "bad-source",
140- restartKind: "full-process",
141- supervisorMode: "external",
142- })}\n`,
143-{ encoding: "utf8", mode: 0o600 },
144-);
218+insertHandoffRow(env, { intentId: "bad", source: "bad-source" });
145219146220expect(readGatewayRestartHandoffSync(env, 1_001)).toBeNull();
147221});
148222149-it("rejects expired and oversized handoff files", () => {
223+it("rejects expired handoff rows", () => {
150224const env = createHandoffEnv();
151225152226expectWrittenHandoff({
@@ -158,53 +232,43 @@ describe("gateway restart handoff", () => {
158232ttlMs: 1_000,
159233});
160234expect(readGatewayRestartHandoffSync(env, 2_001)).toBeNull();
161-162-fs.writeFileSync(handoffPath(env), "x".repeat(8192), { encoding: "utf8", mode: 0o600 });
163-expect(readGatewayRestartHandoffSync(env, 2_001)).toBeNull();
164235});
165236166237it("rejects persisted handoffs with a ttl longer than the supported window", () => {
167238const env = createHandoffEnv();
168239169-fs.writeFileSync(
170-handoffPath(env),
171-`${JSON.stringify({
172- kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND,
173- version: 1,
174- intentId: "too-long",
175- pid: 111,
176- createdAt: 1_000,
177- expiresAt: 61_001,
178- source: "plugin-change",
179- restartKind: "full-process",
180- supervisorMode: "external",
181- })}\n`,
182-{ encoding: "utf8", mode: 0o600 },
183-);
240+insertHandoffRow(env, { intentId: "too-long", createdAt: 1_000, expiresAt: 61_001 });
184241185242expect(readGatewayRestartHandoffSync(env, 1_001)).toBeNull();
186243});
187244188-it("does not follow an existing handoff-path symlink when writing", () => {
245+it("overwrites the previous pending handoff row", () => {
189246const env = createHandoffEnv();
190-const targetPath = path.join(env.OPENCLAW_STATE_DIR ?? "", "attacker-target.txt");
191-fs.writeFileSync(targetPath, "keep", "utf8");
192-try {
193-fs.symlinkSync(targetPath, handoffPath(env));
194-} catch {
195-return;
196-}
197247198248expectWrittenHandoff({
199249 env,
200250pid: 12_345,
201251restartKind: "full-process",
202252supervisorMode: "external",
203253});
254+expectWrittenHandoff({
255+ env,
256+pid: 67_890,
257+reason: "gateway.restart",
258+restartKind: "update-process",
259+supervisorMode: "systemd",
260+});
204261205-expect(fs.readFileSync(targetPath, "utf8")).toBe("keep");
206-expect(fs.lstatSync(handoffPath(env)).isSymbolicLink()).toBe(false);
207-expect(readGatewayRestartHandoffSync(env)?.pid).toBe(12_345);
262+expect(readHandoffRow(env)).toMatchObject({
263+handoff_key: "current",
264+pid: 67_890,
265+reason: "gateway.restart",
266+source: "operator-restart",
267+restart_kind: "update-process",
268+supervisor_mode: "systemd",
269+});
270+expect(readGatewayRestartHandoffSync(env)?.pid).toBe(67_890);
271+expect(fs.existsSync(legacyHandoffPath(env))).toBe(false);
208272});
209273210274it("formats a concise diagnostic line for status surfaces", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。