fix: doctor ack reaction migration (#927)
Thanks @grp06. Co-authored-by: George Pickett <gpickett00@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
- Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.
|
- Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
|
||||||
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
||||||
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
|
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
|
||||||
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
|
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ describe("browser control server", () => {
|
|||||||
fn: "() => 1",
|
fn: "() => 1",
|
||||||
ref: undefined,
|
ref: undefined,
|
||||||
});
|
});
|
||||||
});
|
}, 20_000);
|
||||||
|
|
||||||
it("agent contract: hooks + response + downloads + screenshot", async () => {
|
it("agent contract: hooks + response + downloads + screenshot", async () => {
|
||||||
const base = await startServerAndBase();
|
const base = await startServerAndBase();
|
||||||
|
|||||||
124
src/commands/doctor-legacy-config.test.ts
Normal file
124
src/commands/doctor-legacy-config.test.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
|
||||||
|
|
||||||
|
describe("normalizeLegacyConfigValues", () => {
|
||||||
|
let previousOauthDir: string | undefined;
|
||||||
|
let tempOauthDir: string | undefined;
|
||||||
|
|
||||||
|
const writeCreds = (dir: string) => {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dir, "creds.json"), JSON.stringify({ me: {} }));
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
previousOauthDir = process.env.CLAWDBOT_OAUTH_DIR;
|
||||||
|
tempOauthDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-oauth-"));
|
||||||
|
process.env.CLAWDBOT_OAUTH_DIR = tempOauthDir;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (previousOauthDir === undefined) {
|
||||||
|
delete process.env.CLAWDBOT_OAUTH_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.CLAWDBOT_OAUTH_DIR = previousOauthDir;
|
||||||
|
}
|
||||||
|
if (tempOauthDir) {
|
||||||
|
fs.rmSync(tempOauthDir, { recursive: true, force: true });
|
||||||
|
tempOauthDir = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add whatsapp config when missing and no auth exists", () => {
|
||||||
|
const res = normalizeLegacyConfigValues({
|
||||||
|
messages: { ackReaction: "👀" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config.channels?.whatsapp).toBeUndefined();
|
||||||
|
expect(res.changes).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies legacy ack reaction when whatsapp config exists", () => {
|
||||||
|
const res = normalizeLegacyConfigValues({
|
||||||
|
messages: { ackReaction: "👀", ackReactionScope: "group-mentions" },
|
||||||
|
channels: { whatsapp: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config.channels?.whatsapp?.ackReaction).toEqual({
|
||||||
|
emoji: "👀",
|
||||||
|
direct: false,
|
||||||
|
group: "mentions",
|
||||||
|
});
|
||||||
|
expect(res.changes).toEqual([
|
||||||
|
"Copied messages.ackReaction → channels.whatsapp.ackReaction (scope: group-mentions).",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies legacy ack reaction when whatsapp auth exists", () => {
|
||||||
|
const credsDir = path.join(tempOauthDir ?? "", "whatsapp", "default");
|
||||||
|
writeCreds(credsDir);
|
||||||
|
|
||||||
|
const res = normalizeLegacyConfigValues({
|
||||||
|
messages: { ackReaction: "👀", ackReactionScope: "group-mentions" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config.channels?.whatsapp?.ackReaction).toEqual({
|
||||||
|
emoji: "👀",
|
||||||
|
direct: false,
|
||||||
|
group: "mentions",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies legacy ack reaction when legacy auth exists", () => {
|
||||||
|
const credsPath = path.join(tempOauthDir ?? "", "creds.json");
|
||||||
|
fs.writeFileSync(credsPath, JSON.stringify({ me: {} }));
|
||||||
|
|
||||||
|
const res = normalizeLegacyConfigValues({
|
||||||
|
messages: { ackReaction: "👀", ackReactionScope: "group-mentions" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config.channels?.whatsapp?.ackReaction).toEqual({
|
||||||
|
emoji: "👀",
|
||||||
|
direct: false,
|
||||||
|
group: "mentions",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies legacy ack reaction when non-default auth exists", () => {
|
||||||
|
const credsDir = path.join(tempOauthDir ?? "", "whatsapp", "work");
|
||||||
|
writeCreds(credsDir);
|
||||||
|
|
||||||
|
const res = normalizeLegacyConfigValues({
|
||||||
|
messages: { ackReaction: "👀", ackReactionScope: "group-mentions" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config.channels?.whatsapp?.ackReaction).toEqual({
|
||||||
|
emoji: "👀",
|
||||||
|
direct: false,
|
||||||
|
group: "mentions",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies legacy ack reaction when authDir override exists", () => {
|
||||||
|
const customDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-wa-auth-"));
|
||||||
|
try {
|
||||||
|
writeCreds(customDir);
|
||||||
|
|
||||||
|
const res = normalizeLegacyConfigValues({
|
||||||
|
messages: { ackReaction: "👀", ackReactionScope: "group-mentions" },
|
||||||
|
channels: { whatsapp: { accounts: { work: { authDir: customDir } } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.config.channels?.whatsapp?.ackReaction).toEqual({
|
||||||
|
emoji: "👀",
|
||||||
|
direct: false,
|
||||||
|
group: "mentions",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(customDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
@@ -9,9 +10,12 @@ import {
|
|||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
|
import { resolveOAuthDir } from "../config/paths.js";
|
||||||
|
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
import { resolveWebCredsPath } from "../web/auth-store.js";
|
||||||
|
|
||||||
function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string {
|
function resolveLegacyConfigPath(env: NodeJS.ProcessEnv): string {
|
||||||
const override = env.CLAWDIS_CONFIG_PATH?.trim();
|
const override = env.CLAWDIS_CONFIG_PATH?.trim();
|
||||||
@@ -53,6 +57,48 @@ export function replaceModernName(value: string | undefined): string | undefined
|
|||||||
return value.replace(/clawdbot/g, "clawdis");
|
return value.replace(/clawdbot/g, "clawdis");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasWebCreds(authDir: string): boolean {
|
||||||
|
try {
|
||||||
|
const credsPath = resolveWebCredsPath(authDir);
|
||||||
|
const stats = fs.statSync(credsPath);
|
||||||
|
return stats.isFile() && stats.size > 1;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listWhatsAppAuthDirs(cfg: ClawdbotConfig): string[] {
|
||||||
|
const oauthDir = resolveOAuthDir();
|
||||||
|
const whatsappDir = path.join(oauthDir, "whatsapp");
|
||||||
|
const authDirs = new Set<string>([oauthDir, path.join(whatsappDir, DEFAULT_ACCOUNT_ID)]);
|
||||||
|
|
||||||
|
const accounts = cfg.channels?.whatsapp?.accounts;
|
||||||
|
if (accounts && typeof accounts === "object") {
|
||||||
|
for (const [accountId, accountCfg] of Object.entries(accounts)) {
|
||||||
|
const configured = accountCfg?.authDir?.trim();
|
||||||
|
const authDir = configured ? resolveUserPath(configured) : path.join(whatsappDir, accountId);
|
||||||
|
authDirs.add(authDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = fs.readdirSync(whatsappDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
authDirs.add(path.join(whatsappDir, entry.name));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore missing dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(authDirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasWhatsAppAuthState(cfg: ClawdbotConfig): boolean {
|
||||||
|
const authDirs = listWhatsAppAuthDirs(cfg);
|
||||||
|
return authDirs.some((authDir) => hasWebCreds(authDir));
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
||||||
config: ClawdbotConfig;
|
config: ClawdbotConfig;
|
||||||
changes: string[];
|
changes: string[];
|
||||||
@@ -219,7 +265,9 @@ export function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const legacyAckReaction = cfg.messages?.ackReaction?.trim();
|
const legacyAckReaction = cfg.messages?.ackReaction?.trim();
|
||||||
if (legacyAckReaction) {
|
const hasWhatsAppConfig = cfg.channels?.whatsapp !== undefined;
|
||||||
|
const hasWhatsAppAuth = hasWhatsAppAuthState(cfg);
|
||||||
|
if (legacyAckReaction && (hasWhatsAppConfig || hasWhatsAppAuth)) {
|
||||||
const hasWhatsAppAck = cfg.channels?.whatsapp?.ackReaction !== undefined;
|
const hasWhatsAppAck = cfg.channels?.whatsapp?.ackReaction !== undefined;
|
||||||
if (!hasWhatsAppAck) {
|
if (!hasWhatsAppAck) {
|
||||||
const legacyScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
const legacyScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||||
|
|||||||
Reference in New Issue
Block a user