From d799f846582c0ea8458597f4dd97b79a15e433ce Mon Sep 17 00:00:00 2001 From: gsknnft Date: Mon, 6 Apr 2026 11:12:41 -0400 Subject: [PATCH] fix(claw3doctor): scope provider-specific checks to --profile / --all-profiles flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #101 finding: - Medium: --profile and --all-profiles only scoped the profile health probe loop; OpenClaw/Hermes/Demo/Custom check blocks still ran based on the selected adapter type in runtimeContext, making CLI output misleading. Fix: introduce adapterInScope(adapterType, defaultBehavior) helper in main(). - --profile -> only that adapter's checks run - --all-profiles -> all adapter checks run - no flag -> falls back to existing shouldRun* predicate (unchanged) Also: - Export parseDoctorArgs from claw3doctor-core.mjs (removed duplicate in script) - Add test suites: parseDoctorArgs flag parsing (6 cases) and adapterInScope scoping semantics (4 cases) — 10 new tests, all green --- scripts/claw3doctor.mjs | 48 ++++++++------------- scripts/lib/claw3doctor-core.mjs | 31 ++++++++++++++ tests/unit/claw3doctor.test.ts | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 31 deletions(-) diff --git a/scripts/claw3doctor.mjs b/scripts/claw3doctor.mjs index b195625..dbfd62a 100644 --- a/scripts/claw3doctor.mjs +++ b/scripts/claw3doctor.mjs @@ -14,6 +14,7 @@ import { buildGatewayWarnings, buildOpenClawWarnings, formatDoctorReport, + parseDoctorArgs, resolveRuntimeContext, shouldRunCustomChecks, shouldRunDemoChecks, @@ -106,33 +107,6 @@ const checkFail = (category, label, message, actions) => ({ const trim = (value) => (typeof value === "string" ? value.trim() : ""); -const parseDoctorArgs = (argv) => { - const args = { - json: false, - allProfiles: false, - profile: null, - }; - for (let index = 0; index < argv.length; index += 1) { - const entry = argv[index]; - if (entry === "--json") { - args.json = true; - continue; - } - if (entry === "--all-profiles") { - args.allProfiles = true; - continue; - } - if (entry === "--profile") { - const next = trim(argv[index + 1]).toLowerCase(); - if (next) { - args.profile = next; - index += 1; - } - } - } - return args; -}; - const probeWebSocket = async (url, timeoutMs = 3500) => await new Promise((resolve) => { let settled = false; @@ -285,6 +259,18 @@ async function main() { }); const workspace = detectWorkspaceState(); + /** + * Returns whether provider-specific checks for `adapterType` should run. + * When --profile is set, only that adapter is in scope. + * When --all-profiles is set, all adapters are in scope. + * Otherwise falls back to `defaultBehavior` (the existing shouldRun* predicate). + */ + const adapterInScope = (adapterType, defaultBehavior) => { + if (args.allProfiles) return true; + if (args.profile) return args.profile === adapterType; + return defaultBehavior; + }; + const checks = []; checks.push( @@ -367,7 +353,7 @@ async function main() { checks.push(checkWarn("Gateway access", "Gateway hints", warning)); } - if (runtimeContext.adapterType === "openclaw") { + if (adapterInScope("openclaw", runtimeContext.adapterType === "openclaw")) { for (const warning of buildOpenClawWarnings({ gatewayUrl: runtimeContext.gatewayUrl, tokenConfigured: runtimeContext.tokenConfigured, @@ -378,7 +364,7 @@ async function main() { } } - if (shouldRunCustomChecks({ runtimeContext })) { + if (adapterInScope("custom", shouldRunCustomChecks({ runtimeContext }))) { for (const warning of buildCustomRuntimeWarnings({ gatewayUrl: runtimeContext.gatewayUrl, allowlist: trim(env.CUSTOM_RUNTIME_ALLOWLIST) || trim(env.UPSTREAM_ALLOWLIST), @@ -454,7 +440,7 @@ async function main() { ); } - if (shouldRunDemoChecks({ runtimeContext, env })) { + if (adapterInScope("demo", shouldRunDemoChecks({ runtimeContext, env }))) { const configuredPort = trim(env.DEMO_ADAPTER_PORT) || "18789"; checks.push( checkPass( @@ -466,7 +452,7 @@ async function main() { ); } - if (shouldRunHermesChecks({ runtimeContext, env })) { + if (adapterInScope("hermes", shouldRunHermesChecks({ runtimeContext, env }))) { const hermes = await detectHermesModelHealth(); checks.push( checkPass( diff --git a/scripts/lib/claw3doctor-core.mjs b/scripts/lib/claw3doctor-core.mjs index 75f74ea..9439ec5 100644 --- a/scripts/lib/claw3doctor-core.mjs +++ b/scripts/lib/claw3doctor-core.mjs @@ -437,3 +437,34 @@ export const buildDoctorJsonReport = ({ summary, runtimeContext, paths, checks } fail: checks.filter((check) => check.status === DOCTOR_STATUSES.fail).length, }, }); + +/** + * Parse CLI argv into structured doctor args. + * Exported so the flag behaviour can be unit tested without spawning a process. + */ +export const parseDoctorArgs = (argv) => { + const args = { + json: false, + allProfiles: false, + profile: null, + }; + for (let index = 0; index < argv.length; index += 1) { + const entry = argv[index]; + if (entry === "--json") { + args.json = true; + continue; + } + if (entry === "--all-profiles") { + args.allProfiles = true; + continue; + } + if (entry === "--profile") { + const next = trimString(argv[index + 1] ?? "").toLowerCase(); + if (next) { + args.profile = next; + index += 1; + } + } + } + return args; +}; diff --git a/tests/unit/claw3doctor.test.ts b/tests/unit/claw3doctor.test.ts index 6299927..bf533a6 100644 --- a/tests/unit/claw3doctor.test.ts +++ b/tests/unit/claw3doctor.test.ts @@ -10,6 +10,7 @@ import { DOCTOR_STATUSES, buildGatewayWarnings, formatDoctorReport, + parseDoctorArgs, resolveRuntimeContext, shouldRunCustomChecks, shouldRunDemoChecks, @@ -277,3 +278,74 @@ describe("claw3doctor core", () => { expect(report).toContain("Check counts:"); }); }); + +describe("parseDoctorArgs", () => { + it("returns defaults when no flags are supplied", () => { + expect(parseDoctorArgs([])).toEqual({ json: false, allProfiles: false, profile: null }); + }); + + it("sets json flag", () => { + expect(parseDoctorArgs(["--json"])).toMatchObject({ json: true }); + }); + + it("sets allProfiles flag", () => { + expect(parseDoctorArgs(["--all-profiles"])).toMatchObject({ allProfiles: true, profile: null }); + }); + + it("sets profile to lower-cased value", () => { + expect(parseDoctorArgs(["--profile", "Hermes"])).toMatchObject({ profile: "hermes", allProfiles: false }); + }); + + it("ignores --profile flag when no value follows", () => { + expect(parseDoctorArgs(["--profile"])).toMatchObject({ profile: null }); + }); + + it("combines flags", () => { + expect(parseDoctorArgs(["--json", "--profile", "openclaw"])).toEqual({ + json: true, + allProfiles: false, + profile: "openclaw", + }); + }); +}); + +describe("adapterInScope scoping semantics", () => { + // Mirror the adapterInScope helper used in claw3doctor.mjs so the logic can + // be verified independently of the full CLI entrypoint. + const makeAdapterInScope = + (args: { allProfiles: boolean; profile: string | null }) => + (adapterType: string, defaultBehavior: boolean): boolean => { + if (args.allProfiles) return true; + if (args.profile) return args.profile === adapterType; + return defaultBehavior; + }; + + it("default (no flags): delegates to defaultBehavior", () => { + const inScope = makeAdapterInScope({ allProfiles: false, profile: null }); + expect(inScope("openclaw", true)).toBe(true); + expect(inScope("openclaw", false)).toBe(false); + expect(inScope("hermes", false)).toBe(false); + }); + + it("--profile hermes: only hermes is in scope", () => { + const inScope = makeAdapterInScope({ allProfiles: false, profile: "hermes" }); + expect(inScope("hermes", false)).toBe(true); + expect(inScope("openclaw", true)).toBe(false); // openclaw would default to true but is suppressed + expect(inScope("demo", true)).toBe(false); + expect(inScope("custom", false)).toBe(false); + }); + + it("--profile openclaw: only openclaw is in scope", () => { + const inScope = makeAdapterInScope({ allProfiles: false, profile: "openclaw" }); + expect(inScope("openclaw", false)).toBe(true); + expect(inScope("hermes", true)).toBe(false); + }); + + it("--all-profiles: every adapter is in scope regardless of default", () => { + const inScope = makeAdapterInScope({ allProfiles: true, profile: null }); + expect(inScope("hermes", false)).toBe(true); + expect(inScope("openclaw", false)).toBe(true); + expect(inScope("demo", false)).toBe(true); + expect(inScope("custom", false)).toBe(true); + }); +});