Skip to content

Commit 41c0540

Browse files
Pick-catvincentkoc
andauthored
fix(doctor): expose default account routing in lint (#96147)
Co-authored-by: Vincent Koc <[email protected]> Co-authored-by: Pick-cat <[email protected]>
1 parent 048fe08 commit 41c0540

3 files changed

Lines changed: 122 additions & 15 deletions

File tree

src/commands/doctor-config-flow.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
noteOpencodeProviderOverrides,
1212
} from "./doctor-config-analysis.js";
1313
import { runDoctorConfigPreflight } from "./doctor-config-preflight.js";
14-
import { normalizeCompatibilityConfigValues } from "./doctor/shared/legacy-config-core-migrate.js";
1514
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
1615
import { emitDoctorNotes, sanitizeDoctorNote } from "./doctor/emit-notes.js";
1716
import { finalizeDoctorConfigFlow } from "./doctor/finalize-config-flow.js";
@@ -20,10 +19,7 @@ import {
2019
applyUnknownConfigKeyStep,
2120
} from "./doctor/shared/config-flow-steps.js";
2221
import { applyDoctorConfigMutation } from "./doctor/shared/config-mutation-state.js";
23-
import {
24-
collectMissingDefaultAccountBindingWarnings,
25-
collectMissingExplicitDefaultAccountWarnings,
26-
} from "./doctor/shared/default-account-warnings.js";
22+
import { normalizeCompatibilityConfigValues } from "./doctor/shared/legacy-config-core-migrate.js";
2723

2824
function hasLegacyInternalHookHandlers(raw: unknown): boolean {
2925
const handlers = (raw as { hooks?: { internal?: { handlers?: unknown } } })?.hooks?.internal
@@ -289,16 +285,6 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
289285
}
290286
}
291287

292-
const missingDefaultAccountBindingWarnings =
293-
collectMissingDefaultAccountBindingWarnings(candidate);
294-
if (missingDefaultAccountBindingWarnings.length > 0) {
295-
note(missingDefaultAccountBindingWarnings.join("\n"), "Doctor warnings");
296-
}
297-
const missingExplicitDefaultWarnings = collectMissingExplicitDefaultAccountWarnings(candidate);
298-
if (missingExplicitDefaultWarnings.length > 0) {
299-
note(missingExplicitDefaultWarnings.join("\n"), "Doctor warnings");
300-
}
301-
302288
const { repairHooksTokenReuseGatewayAuth } =
303289
await import("./doctor/shared/hooks-token-reuse-repair.js");
304290
const hooksTokenReuseRepair = await repairHooksTokenReuseGatewayAuth(candidate, process.env);

src/flows/doctor-health-contributions.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import os from "node:os";
44
import nodePath from "node:path";
55
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
66
import type { DoctorPrompter } from "../commands/doctor-prompter.js";
7+
import type { OpenClawConfig } from "../config/types.openclaw.js";
78
import { CORE_HEALTH_CHECKS } from "./doctor-core-checks.js";
89
import "./doctor-tool-result-cap-advice.js";
910
import {
@@ -1331,6 +1332,44 @@ describe("doctor health contributions", () => {
13311332
});
13321333
});
13331334

1335+
it("keeps default-account routing lint opt-in", async () => {
1336+
const contribution = requireDoctorContribution("doctor:default-account-routing");
1337+
const check = contribution.healthChecks[0] as HealthCheck | undefined;
1338+
expect(check).toMatchObject({ defaultEnabled: false });
1339+
1340+
const ctx = {
1341+
cfg: {
1342+
channels: {
1343+
telegram: {
1344+
accounts: {
1345+
alerts: {},
1346+
work: {},
1347+
},
1348+
},
1349+
},
1350+
bindings: [{ agentId: "ops", match: { channel: "telegram" } }],
1351+
} as unknown as OpenClawConfig,
1352+
mode: "lint" as const,
1353+
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
1354+
};
1355+
1356+
await expect(runDoctorLintChecks(ctx, { checks: [check!] })).resolves.toMatchObject({
1357+
checksRun: 0,
1358+
checksSkipped: 1,
1359+
findings: [],
1360+
});
1361+
await expect(
1362+
runDoctorLintChecks(ctx, { checks: [check!], includeAllChecks: true }),
1363+
).resolves.toMatchObject({
1364+
checksRun: 1,
1365+
checksSkipped: 0,
1366+
findings: [
1367+
expect.objectContaining({ checkId: "core/doctor/default-account-routing" }),
1368+
expect.objectContaining({ checkId: "core/doctor/default-account-routing" }),
1369+
],
1370+
});
1371+
});
1372+
13341373
it("preserves allow-exec Gateway SecretRef resolution in auth health", async () => {
13351374
const contribution = requireDoctorContribution("doctor:gateway-auth");
13361375
const ctx = {
@@ -2735,6 +2774,65 @@ describe("doctor health contributions", () => {
27352774
);
27362775
});
27372776

2777+
it.each([false, true])(
2778+
"reports default-account routing warnings during doctor runs (repair=%s)",
2779+
async (shouldRepair) => {
2780+
const contribution = requireDoctorContribution("doctor:default-account-routing");
2781+
mocks.runDoctorHealthRepairs.mockImplementation(async (ctx, options) => {
2782+
const findings = await options.checks[0]!.detect(ctx);
2783+
return {
2784+
config: ctx.cfg,
2785+
findings,
2786+
remainingFindings: findings,
2787+
changes: [],
2788+
warnings: [],
2789+
diffs: [],
2790+
effects: [],
2791+
checksRun: 1,
2792+
checksRepaired: 0,
2793+
checksValidated: 1,
2794+
};
2795+
});
2796+
const ctx = {
2797+
cfg: {
2798+
channels: {
2799+
telegram: {
2800+
accounts: {
2801+
alerts: {},
2802+
work: {},
2803+
},
2804+
},
2805+
},
2806+
bindings: [{ agentId: "ops", match: { channel: "telegram" } }],
2807+
} as unknown as OpenClawConfig,
2808+
configResult: { cfg: {} },
2809+
sourceConfigValid: true,
2810+
prompter: buildDoctorPrompter(shouldRepair),
2811+
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
2812+
options: {},
2813+
cfgForPersistence: {},
2814+
configPath: "/tmp/fake-openclaw.json",
2815+
env: {},
2816+
} as unknown as Parameters<(typeof contribution)["run"]>[0];
2817+
2818+
await contribution.run(ctx);
2819+
2820+
expect(mocks.runDoctorHealthRepairs).toHaveBeenCalledWith(
2821+
expect.objectContaining({ mode: "fix", dryRun: !shouldRepair }),
2822+
expect.objectContaining({
2823+
checks: contribution.healthChecks,
2824+
dryRun: !shouldRepair,
2825+
}),
2826+
);
2827+
expect(ctx.runtime.log).toHaveBeenCalledWith(
2828+
expect.stringContaining("accounts.default is missing and no valid account-scoped binding"),
2829+
);
2830+
expect(ctx.runtime.log).toHaveBeenCalledWith(
2831+
expect.stringContaining("multiple accounts are configured but no explicit default is set"),
2832+
);
2833+
},
2834+
);
2835+
27382836
it("skips doctor config writes under legacy update parents", () => {
27392837
expect(
27402838
shouldSkipLegacyUpdateDoctorConfigWrite({

src/flows/doctor-health-contributions.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,6 +1915,29 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] {
19151915
],
19161916
run: runGatewayServicesHealth,
19171917
}),
1918+
createDoctorHealthContribution({
1919+
id: "doctor:default-account-routing",
1920+
label: "Default account routing",
1921+
healthChecks: {
1922+
id: "core/doctor/default-account-routing",
1923+
description: "Multi-account channels have explicit default routing or complete bindings.",
1924+
defaultEnabled: false,
1925+
async detect(ctx) {
1926+
const {
1927+
collectMissingDefaultAccountBindingWarnings,
1928+
collectMissingExplicitDefaultAccountWarnings,
1929+
} = await import("../commands/doctor/shared/default-account-warnings.js");
1930+
return [
1931+
...collectMissingDefaultAccountBindingWarnings(ctx.cfg),
1932+
...collectMissingExplicitDefaultAccountWarnings(ctx.cfg),
1933+
].map((message) => ({
1934+
checkId: "core/doctor/default-account-routing",
1935+
severity: "warning" as const,
1936+
message: message.replace(/^- /, "").trim(),
1937+
}));
1938+
},
1939+
},
1940+
}),
19181941
createDoctorHealthContribution({
19191942
id: "doctor:startup-channel-maintenance",
19201943
label: "Startup channel maintenance",

0 commit comments

Comments
 (0)