refactor(doctor): model legacy file copies as plans

This commit is contained in:
Peter Steinberger
2026-03-08 02:07:17 +00:00
parent 01cff3a7a6
commit 44e7c1142e
2 changed files with 109 additions and 26 deletions

View File

@@ -296,6 +296,9 @@ describe("doctor legacy state migrations", () => {
env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv,
});
expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true);
expect(
detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)),
).toEqual(["telegram-default-allowFrom.json"]);
const result = await runLegacyStateMigrations({ detected, now: () => 123 });
expect(result.warnings).toEqual([]);
@@ -308,6 +311,59 @@ describe("doctor legacy state migrations", () => {
});
});
it("fans out legacy Telegram pairing allowFrom store to configured named accounts", async () => {
const root = await makeTempRoot();
const cfg: OpenClawConfig = {
channels: {
telegram: {
accounts: {
bot1: {},
bot2: {},
},
},
},
};
const oauthDir = ensureCredentialsDir(root);
fs.writeFileSync(
path.join(oauthDir, "telegram-allowFrom.json"),
JSON.stringify(
{
version: 1,
allowFrom: ["123456"],
},
null,
2,
) + "\n",
"utf-8",
);
const detected = await detectLegacyStateMigrations({
cfg,
env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv,
});
expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true);
expect(
detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)).toSorted(),
).toEqual(["telegram-bot1-allowFrom.json", "telegram-bot2-allowFrom.json"]);
const result = await runLegacyStateMigrations({ detected, now: () => 123 });
expect(result.warnings).toEqual([]);
const bot1Target = path.join(oauthDir, "telegram-bot1-allowFrom.json");
const bot2Target = path.join(oauthDir, "telegram-bot2-allowFrom.json");
expect(fs.existsSync(bot1Target)).toBe(true);
expect(fs.existsSync(bot2Target)).toBe(true);
expect(fs.existsSync(path.join(oauthDir, "telegram-default-allowFrom.json"))).toBe(false);
expect(JSON.parse(fs.readFileSync(bot1Target, "utf-8"))).toEqual({
version: 1,
allowFrom: ["123456"],
});
expect(JSON.parse(fs.readFileSync(bot2Target, "utf-8"))).toEqual({
version: 1,
allowFrom: ["123456"],
});
});
it("no-ops when nothing detected", async () => {
const root = await makeTempRoot();
const cfg: OpenClawConfig = {};

View File

@@ -21,6 +21,7 @@ import {
DEFAULT_MAIN_KEY,
normalizeAgentId,
} from "../routing/session-key.js";
import { listTelegramAccountIds } from "../telegram/accounts.js";
import { isWithinDir } from "./path-safety.js";
import {
ensureDir,
@@ -57,13 +58,18 @@ export type LegacyStateDetection = {
hasLegacy: boolean;
};
pairingAllowFrom: {
legacyTelegramPath: string;
targetTelegramPath: string;
hasLegacyTelegram: boolean;
copyPlans: FileCopyPlan[];
};
preview: string[];
};
type FileCopyPlan = {
label: string;
sourcePath: string;
targetPath: string;
};
type MigrationLogger = {
info: (message: string) => void;
warn: (message: string) => void;
@@ -98,6 +104,30 @@ function isLegacyGroupKey(key: string): boolean {
return false;
}
function buildFileCopyPreview(plan: FileCopyPlan): string {
return `- ${plan.label}: ${plan.sourcePath}${plan.targetPath}`;
}
async function runFileCopyPlans(
plans: FileCopyPlan[],
): Promise<{ changes: string[]; warnings: string[] }> {
const changes: string[] = [];
const warnings: string[] = [];
for (const plan of plans) {
if (fileExists(plan.targetPath)) {
continue;
}
try {
ensureDir(path.dirname(plan.targetPath));
fs.copyFileSync(plan.sourcePath, plan.targetPath);
changes.push(`Copied ${plan.label}${plan.targetPath}`);
} catch (err) {
warnings.push(`Failed migrating ${plan.label} (${plan.sourcePath}): ${String(err)}`);
}
}
return { changes, warnings };
}
function canonicalizeSessionKeyForAgent(params: {
key: string;
agentId: string;
@@ -619,13 +649,24 @@ export async function detectLegacyStateMigrations(params: {
fileExists(path.join(oauthDir, "creds.json")) &&
!fileExists(path.join(targetWhatsAppAuthDir, "creds.json"));
const legacyTelegramAllowFromPath = resolveChannelAllowFromPath("telegram", env);
const targetTelegramAllowFromPath = resolveChannelAllowFromPath(
"telegram",
env,
DEFAULT_ACCOUNT_ID,
);
const hasLegacyTelegramAllowFrom =
fileExists(legacyTelegramAllowFromPath) && !fileExists(targetTelegramAllowFromPath);
const telegramPairingAllowFromPlans = fileExists(legacyTelegramAllowFromPath)
? Array.from(
new Set(
listTelegramAccountIds(params.cfg).map((accountId) =>
resolveChannelAllowFromPath("telegram", env, accountId),
),
),
)
.filter((targetPath) => !fileExists(targetPath))
.map(
(targetPath): FileCopyPlan => ({
label: "Telegram pairing allowFrom",
sourcePath: legacyTelegramAllowFromPath,
targetPath,
}),
)
: [];
const hasLegacyTelegramAllowFrom = telegramPairingAllowFromPlans.length > 0;
const preview: string[] = [];
if (hasLegacySessions) {
@@ -641,9 +682,7 @@ export async function detectLegacyStateMigrations(params: {
preview.push(`- WhatsApp auth: ${oauthDir}${targetWhatsAppAuthDir} (keep oauth.json)`);
}
if (hasLegacyTelegramAllowFrom) {
preview.push(
`- Telegram pairing allowFrom: ${legacyTelegramAllowFromPath}${targetTelegramAllowFromPath}`,
);
preview.push(...telegramPairingAllowFromPlans.map(buildFileCopyPreview));
}
return {
@@ -671,9 +710,8 @@ export async function detectLegacyStateMigrations(params: {
hasLegacy: hasLegacyWhatsAppAuth,
},
pairingAllowFrom: {
legacyTelegramPath: legacyTelegramAllowFromPath,
targetTelegramPath: targetTelegramAllowFromPath,
hasLegacyTelegram: hasLegacyTelegramAllowFrom,
copyPlans: telegramPairingAllowFromPlans,
},
preview,
};
@@ -899,18 +937,7 @@ async function migrateLegacyTelegramPairingAllowFrom(
if (!detected.pairingAllowFrom.hasLegacyTelegram) {
return { changes, warnings };
}
const legacyPath = detected.pairingAllowFrom.legacyTelegramPath;
const targetPath = detected.pairingAllowFrom.targetTelegramPath;
try {
ensureDir(path.dirname(targetPath));
fs.copyFileSync(legacyPath, targetPath);
changes.push(`Copied Telegram pairing allowFrom → ${targetPath}`);
} catch (err) {
warnings.push(`Failed migrating Telegram pairing allowFrom (${legacyPath}): ${String(err)}`);
}
return { changes, warnings };
return await runFileCopyPlans(detected.pairingAllowFrom.copyPlans);
}
export async function runLegacyStateMigrations(params: {