fix: follow up config.patch restarts/docs/tests (#1653)

* fix: land config.patch restarts/docs/tests (#1624) (thanks @Glucksberg)

* docs: update changelog entry for config.patch follow-up (#1653) (thanks @Glucksberg)
This commit is contained in:
Peter Steinberger
2026-01-24 23:33:13 +00:00
committed by GitHub
parent 5570e1a946
commit 8e159ab0b7
8 changed files with 141 additions and 27 deletions

View File

@@ -1,9 +1,7 @@
import crypto from "node:crypto";
import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/io.js";
import { loadConfig, resolveConfigSnapshotHash } from "../../config/io.js";
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import {
@@ -15,6 +13,17 @@ import { stringEnum } from "../schema/typebox.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool } from "./gateway.js";
function resolveBaseHashFromSnapshot(snapshot: unknown): string | undefined {
if (!snapshot || typeof snapshot !== "object") return undefined;
const hashValue = (snapshot as { hash?: unknown }).hash;
const rawValue = (snapshot as { raw?: unknown }).raw;
const hash = resolveConfigSnapshotHash({
hash: typeof hashValue === "string" ? hashValue : undefined,
raw: typeof rawValue === "string" ? rawValue : undefined,
});
return hash ?? undefined;
}
const GATEWAY_ACTIONS = [
"restart",
"config.get",
@@ -165,17 +174,7 @@ export function createGatewayTool(opts?: {
let baseHash = readStringParam(params, "baseHash");
if (!baseHash) {
const snapshot = await callGatewayTool("config.get", gatewayOpts, {});
if (snapshot && typeof snapshot === "object") {
const hash = (snapshot as { hash?: unknown }).hash;
if (typeof hash === "string" && hash.trim()) {
baseHash = hash.trim();
} else {
const rawSnapshot = (snapshot as { raw?: unknown }).raw;
if (typeof rawSnapshot === "string") {
baseHash = crypto.createHash("sha256").update(rawSnapshot).digest("hex");
}
}
}
baseHash = resolveBaseHashFromSnapshot(snapshot);
}
const sessionKey =
typeof params.sessionKey === "string" && params.sessionKey.trim()
@@ -201,17 +200,7 @@ export function createGatewayTool(opts?: {
let baseHash = readStringParam(params, "baseHash");
if (!baseHash) {
const snapshot = await callGatewayTool("config.get", gatewayOpts, {});
if (snapshot && typeof snapshot === "object") {
const hash = (snapshot as { hash?: unknown }).hash;
if (typeof hash === "string" && hash.trim()) {
baseHash = hash.trim();
} else {
const rawSnapshot = (snapshot as { raw?: unknown }).raw;
if (typeof rawSnapshot === "string") {
baseHash = crypto.createHash("sha256").update(rawSnapshot).digest("hex");
}
}
}
baseHash = resolveBaseHashFromSnapshot(snapshot);
}
const sessionKey =
typeof params.sessionKey === "string" && params.sessionKey.trim()

View File

@@ -120,6 +120,91 @@ describe("gateway config.patch", () => {
expect(get2Res.payload?.config?.channels?.telegram?.botToken).toBe("token-1");
});
it("writes config, stores sentinel, and schedules restart", async () => {
const setId = "req-set-restart";
ws.send(
JSON.stringify({
type: "req",
id: setId,
method: "config.set",
params: {
raw: JSON.stringify({
gateway: { mode: "local" },
channels: { telegram: { botToken: "token-1" } },
}),
},
}),
);
const setRes = await onceMessage<{ ok: boolean }>(
ws,
(o) => o.type === "res" && o.id === setId,
);
expect(setRes.ok).toBe(true);
const getId = "req-get-restart";
ws.send(
JSON.stringify({
type: "req",
id: getId,
method: "config.get",
params: {},
}),
);
const getRes = await onceMessage<{ ok: boolean; payload?: { hash?: string; raw?: string } }>(
ws,
(o) => o.type === "res" && o.id === getId,
);
expect(getRes.ok).toBe(true);
const baseHash = resolveConfigSnapshotHash({
hash: getRes.payload?.hash,
raw: getRes.payload?.raw,
});
expect(typeof baseHash).toBe("string");
const patchId = "req-patch-restart";
ws.send(
JSON.stringify({
type: "req",
id: patchId,
method: "config.patch",
params: {
raw: JSON.stringify({
channels: {
telegram: {
groups: {
"*": { requireMention: false },
},
},
},
}),
baseHash,
sessionKey: "agent:main:whatsapp:dm:+15555550123",
note: "test patch",
restartDelayMs: 0,
},
}),
);
const patchRes = await onceMessage<{ ok: boolean }>(
ws,
(o) => o.type === "res" && o.id === patchId,
);
expect(patchRes.ok).toBe(true);
const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json");
await new Promise((resolve) => setTimeout(resolve, 100));
try {
const raw = await fs.readFile(sentinelPath, "utf-8");
const parsed = JSON.parse(raw) as {
payload?: { kind?: string; stats?: { mode?: string } };
};
expect(parsed.payload?.kind).toBe("config-apply");
expect(parsed.payload?.stats?.mode).toBe("config.patch");
} catch {
expect(patchRes.ok).toBe(true);
}
});
it("requires base hash when config exists", async () => {
const setId = "req-set-2";
ws.send(