perf: isolate doctor core check tests (#84493)

Merged via squash.

Prepared head SHA: 6229656ba1
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
This commit is contained in:
Frank Yang
2026-05-21 10:47:43 +08:00
committed by GitHub
parent 3d3cf96dc9
commit 2585249737
7 changed files with 454 additions and 211 deletions

View File

@@ -2,6 +2,12 @@
Docs: https://docs.openclaw.ai
## Unreleased
### Changes
- Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so `doctor-core-checks` no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.
## 2026.5.20
### Changes
@@ -68,7 +74,6 @@ Docs: https://docs.openclaw.ai
- CLI/gateway: include the running Gateway version in `gateway status` JSON output, preserving existing server metadata while falling back to status RPC data for read probes. Fixes #56222. Thanks @galiniliev.
- Memory/search: close local embedding providers when active-memory searches time out so pending local model loads and embedding contexts are aborted and released. (#83858) Thanks @brokemac79.
- CLI/nodes: request pending node surface approval scopes before `openclaw nodes approve` so exec-capable node approval can use admin-scoped Gateway credentials instead of failing with `missing scope: operator.admin`. (#84392) Thanks @joshavant.
- Agents: include bounded trajectory queued-writer diagnostics in `pi-trajectory-flush` timeout warnings so flush stalls show pending writes, queued bytes, and append state. Fixes #82961. (#82962) Thanks @galiniliev.
- Agents/subagents: recover stale completion announces by retrying unsupported transcript-wait wakes without transcript waiting and forcing a message-tool handoff when the requester run is already stale. Fixes #83699. (#83700) Thanks @galiniliev.
- Agents/subagents: constrain wildcard subagent target allowlists to configured agents while preserving explicitly listed compatibility targets. Fixes #84040. (#84357) Thanks @joshavant.

View File

@@ -0,0 +1,35 @@
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
export function collectUnavailableAgentSkills(report: SkillStatusReport): SkillStatusEntry[] {
return report.skills.filter(
(skill) =>
!skill.eligible &&
!skill.disabled &&
!skill.blockedByAllowlist &&
!skill.blockedByAgentFilter,
);
}
export function disableUnavailableSkillsInConfig(
config: OpenClawConfig,
skills: readonly SkillStatusEntry[],
): OpenClawConfig {
if (skills.length === 0) {
return config;
}
const entries = { ...config.skills?.entries };
for (const skill of skills) {
entries[skill.skillKey] = {
...entries[skill.skillKey],
enabled: false,
};
}
return {
...config,
skills: {
...config.skills,
entries,
},
};
}

View File

@@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
import type { SkillStatusEntry } from "../agents/skills-status.js";
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
import {
detectGhConfigDirMismatch,
@@ -12,16 +12,15 @@ import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { note } from "../terminal/note.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
import {
collectUnavailableAgentSkills,
disableUnavailableSkillsInConfig,
} from "./doctor-skills-core.js";
export function collectUnavailableAgentSkills(report: SkillStatusReport): SkillStatusEntry[] {
return report.skills.filter(
(skill) =>
!skill.eligible &&
!skill.disabled &&
!skill.blockedByAllowlist &&
!skill.blockedByAgentFilter,
);
}
export {
collectUnavailableAgentSkills,
disableUnavailableSkillsInConfig,
} from "./doctor-skills-core.js";
function formatMissingSummary(skill: SkillStatusEntry): string {
const missing: string[] = [];
@@ -100,29 +99,6 @@ export function formatUnavailableSkillDoctorLines(skills: SkillStatusEntry[]): s
return lines;
}
export function disableUnavailableSkillsInConfig(
config: OpenClawConfig,
skills: readonly SkillStatusEntry[],
): OpenClawConfig {
if (skills.length === 0) {
return config;
}
const entries = { ...config.skills?.entries };
for (const skill of skills) {
entries[skill.skillKey] = {
...entries[skill.skillKey],
enabled: false,
};
}
return {
...config,
skills: {
...config.skills,
entries,
},
};
}
export async function maybeRepairSkillReadiness(params: {
cfg: OpenClawConfig;
prompter: DoctorPrompter;

View File

@@ -0,0 +1,114 @@
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { CORE_HEALTH_CHECKS } from "./doctor-core-checks.js";
import type { HealthCheck } from "./health-checks.js";
const runtime = { log() {}, error() {}, exit() {} };
function getCheck(id: string): HealthCheck {
const check = CORE_HEALTH_CHECKS.find((entry) => entry.id === id);
if (!check) {
throw new Error(`Missing health check ${id}`);
}
return check;
}
describe("doctor core skills readiness smoke", () => {
let tmp: string | undefined;
afterEach(async () => {
if (tmp !== undefined) {
await fs.rm(tmp, { recursive: true, force: true });
tmp = undefined;
}
});
it("detects and repairs a real unavailable workspace skill", async () => {
tmp = await fs.mkdtemp(join(tmpdir(), "openclaw-health-skills-"));
const skillDir = join(tmp, "skills", "missing-tool");
await fs.mkdir(skillDir, { recursive: true });
await fs.writeFile(
join(skillDir, "SKILL.md"),
`---
name: missing-tool
description: Missing tool
metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}}'
---
# Missing tool
`,
"utf-8",
);
const cfg: OpenClawConfig = {
agents: {
defaults: {
workspace: tmp,
skills: ["missing-tool"],
},
},
};
const check = getCheck("core/doctor/skills-readiness");
const findings = await check.detect({
mode: "lint",
runtime,
cfg,
cwd: tmp,
});
expect(findings).toContainEqual(
expect.objectContaining({
checkId: "core/doctor/skills-readiness",
severity: "warning",
path: "skills.entries.missing-tool.enabled",
}),
);
await expect(
check.detect(
{
mode: "fix",
runtime,
cfg,
cwd: tmp,
},
{ paths: ["skills.entries.other-tool.enabled"] },
),
).resolves.toEqual([]);
await expect(
check.detect(
{
mode: "fix",
runtime,
cfg,
cwd: tmp,
},
{ paths: ["skills.entries.missing-tool.enabled"] },
),
).resolves.toContainEqual(
expect.objectContaining({
path: "skills.entries.missing-tool.enabled",
}),
);
const repaired = await check.repair?.(
{
mode: "fix",
runtime,
cfg,
cwd: tmp,
},
findings,
);
expect(repaired?.config?.skills?.entries?.["missing-tool"]).toEqual({ enabled: false });
expect(repaired?.changes).toContain("Disabled unavailable skill missing-tool.");
expect(repaired?.effects).toContainEqual(
expect.objectContaining({
kind: "config",
action: "disable-skill",
target: "skills.entries.missing-tool.enabled",
}),
);
});
});

View File

@@ -0,0 +1,14 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { buildWorkspaceSkillStatus, type SkillStatusEntry } from "../agents/skills-status.js";
import { collectUnavailableAgentSkills } from "../commands/doctor-skills-core.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
export function detectUnavailableSkills(cfg: OpenClawConfig): SkillStatusEntry[] {
const agentId = resolveDefaultAgentId(cfg);
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const report = buildWorkspaceSkillStatus(workspaceDir, {
config: cfg,
agentId,
});
return collectUnavailableAgentSkills(report);
}

View File

@@ -1,10 +1,10 @@
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import type { SkillStatusEntry } from "../agents/skills-status.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
CORE_HEALTH_CHECKS,
createCoreHealthChecks,
type CoreHealthCheckDeps,
registerCoreHealthChecks,
resetCoreHealthChecksForTest,
} from "./doctor-core-checks.js";
@@ -14,22 +14,76 @@ import {
listHealthChecks,
registerHealthCheck,
} from "./health-check-registry.js";
import type { HealthCheck } from "./health-checks.js";
const runtime = { log() {}, error() {}, exit() {} };
function createSkill(overrides: Partial<SkillStatusEntry> = {}): SkillStatusEntry {
return {
name: "missing-tool",
description: "Missing tool",
source: "workspace",
bundled: false,
filePath: "/tmp/openclaw-test-workspace/skills/missing-tool/SKILL.md",
baseDir: "/tmp/openclaw-test-workspace/skills/missing-tool",
skillKey: "missing-tool",
always: false,
disabled: false,
blockedByAllowlist: false,
blockedByAgentFilter: false,
eligible: false,
modelVisible: false,
userInvocable: true,
commandVisible: false,
requirements: {
bins: ["openclaw-test-missing-skill-bin"],
anyBins: [],
env: [],
config: [],
os: [],
},
missing: {
bins: ["openclaw-test-missing-skill-bin"],
anyBins: [],
env: [],
config: [],
os: [],
},
configChecks: [],
install: [],
...overrides,
};
}
function createDeps(overrides: Partial<CoreHealthCheckDeps> = {}): CoreHealthCheckDeps {
return {
async detectUnavailableSkills(): Promise<readonly SkillStatusEntry[]> {
return [];
},
async collectSecurityWarnings(): Promise<readonly string[]> {
return [];
},
async collectWorkspaceSuggestionNotes(): Promise<readonly string[]> {
return [];
},
...overrides,
};
}
function getCheck(checks: readonly HealthCheck[], id: string): HealthCheck {
const check = checks.find((entry) => entry.id === id);
if (!check) {
throw new Error(`Missing health check ${id}`);
}
return check;
}
describe("registerCoreHealthChecks", () => {
let tmp: string | undefined;
beforeEach(() => {
clearHealthChecksForTest();
resetCoreHealthChecksForTest();
});
afterEach(async () => {
if (tmp !== undefined) {
await fs.rm(tmp, { recursive: true, force: true });
tmp = undefined;
}
});
it("registers the built-in health checks once", () => {
registerCoreHealthChecks();
registerCoreHealthChecks();
@@ -88,39 +142,34 @@ describe("registerCoreHealthChecks", () => {
).toBe(false);
});
it("shows the repair-capable health check shape with skills readiness", async () => {
tmp = await fs.mkdtemp(join(tmpdir(), "openclaw-health-skills-"));
const skillDir = join(tmp, "skills", "missing-tool");
await fs.mkdir(skillDir, { recursive: true });
await fs.writeFile(
join(skillDir, "SKILL.md"),
`---
name: missing-tool
description: Missing tool
metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}}'
---
# Missing tool
`,
"utf-8",
);
it("converts unavailable skills into repair-capable health findings", async () => {
const unavailableSkill = createSkill();
const cfg: OpenClawConfig = {
agents: {
defaults: {
workspace: tmp,
workspace: "/tmp/openclaw-test-workspace",
skills: ["missing-tool"],
},
},
};
const check = CORE_HEALTH_CHECKS.find((entry) => entry.id === "core/doctor/skills-readiness");
const check = getCheck(
createCoreHealthChecks(
createDeps({
async detectUnavailableSkills(): Promise<readonly SkillStatusEntry[]> {
return [unavailableSkill];
},
}),
),
"core/doctor/skills-readiness",
);
expect(check?.repair).toBeTypeOf("function");
expect(check.repair).toBeTypeOf("function");
const findings = await check?.detect({
const findings = await check.detect({
mode: "lint",
runtime: { log() {}, error() {}, exit() {} },
runtime,
cfg,
cwd: tmp,
cwd: "/tmp/openclaw-test-workspace",
});
expect(findings).toContainEqual(
expect.objectContaining({
@@ -130,23 +179,23 @@ metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}
}),
);
await expect(
check?.detect(
check.detect(
{
mode: "fix",
runtime: { log() {}, error() {}, exit() {} },
runtime,
cfg,
cwd: tmp,
cwd: "/tmp/openclaw-test-workspace",
},
{ paths: ["skills.entries.other-tool.enabled"] },
),
).resolves.toEqual([]);
await expect(
check?.detect(
check.detect(
{
mode: "fix",
runtime: { log() {}, error() {}, exit() {} },
runtime,
cfg,
cwd: tmp,
cwd: "/tmp/openclaw-test-workspace",
},
{ paths: ["skills.entries.missing-tool.enabled"] },
),
@@ -156,14 +205,14 @@ metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}
}),
);
const repaired = await check?.repair?.(
const repaired = await check.repair?.(
{
mode: "fix",
runtime: { log() {}, error() {}, exit() {} },
runtime,
cfg,
cwd: tmp,
cwd: "/tmp/openclaw-test-workspace",
},
findings ?? [],
findings,
);
expect(repaired?.config?.skills?.entries?.["missing-tool"]).toEqual({ enabled: false });
expect(repaired?.changes).toContain("Disabled unavailable skill missing-tool.");
@@ -177,11 +226,23 @@ metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}
});
it("converts security doctor warnings into health findings", async () => {
const check = CORE_HEALTH_CHECKS.find((entry) => entry.id === "core/doctor/security");
const check = getCheck(
createCoreHealthChecks(
createDeps({
async collectSecurityWarnings(): Promise<readonly string[]> {
return [
'- CRITICAL: Gateway bound to "lan" (0.0.0.0) without authentication.',
'- WARNING: Gateway bound to "lan" (0.0.0.0).',
];
},
}),
),
"core/doctor/security",
);
const findings = await check?.detect({
const findings = await check.detect({
mode: "lint",
runtime: { log() {}, error() {}, exit() {} },
runtime,
cfg: {
gateway: {
bind: "lan",
@@ -199,25 +260,44 @@ metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}
message: expect.stringContaining("Gateway bound"),
}),
);
expect(findings).toContainEqual(
expect.objectContaining({
checkId: "core/doctor/security",
severity: "warning",
message: expect.stringContaining("Gateway bound"),
}),
);
});
it("converts workspace suggestions into info findings", async () => {
tmp = await fs.mkdtemp(join(tmpdir(), "openclaw-health-workspace-"));
const check = CORE_HEALTH_CHECKS.find(
(entry) => entry.id === "core/doctor/workspace-suggestions",
const check = getCheck(
createCoreHealthChecks(
createDeps({
async collectWorkspaceSuggestionNotes(): Promise<readonly string[]> {
return [
[
"- Tip: back up the workspace in a private git repo (GitHub or GitLab).",
"- Keep ~/.openclaw out of git; it contains credentials and session history.",
].join("\n"),
"Memory system not found in workspace.",
];
},
}),
),
"core/doctor/workspace-suggestions",
);
const findings = await check?.detect({
const findings = await check.detect({
mode: "lint",
runtime: { log() {}, error() {}, exit() {} },
runtime,
cfg: {
agents: {
defaults: {
workspace: tmp,
workspace: "/tmp/openclaw-test-workspace",
},
},
},
cwd: tmp,
cwd: "/tmp/openclaw-test-workspace",
});
expect(findings).toContainEqual(

View File

@@ -1,16 +1,13 @@
import path from "node:path";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { buildWorkspaceSkillStatus, type SkillStatusEntry } from "../agents/skills-status.js";
import type { SkillStatusEntry } from "../agents/skills-status.js";
import {
detectLegacyClawdBrowserProfileResidue,
maybeArchiveLegacyClawdBrowserProfileResidue,
type LegacyClawdBrowserProfileResidue,
} from "../commands/doctor-browser.js";
import { hasConfiguredCommandOwners } from "../commands/doctor-command-owner.js";
import {
collectUnavailableAgentSkills,
disableUnavailableSkillsInConfig,
} from "../commands/doctor-skills.js";
import { disableUnavailableSkillsInConfig } from "../commands/doctor-skills-core.js";
import type { ConfigValidationIssue, OpenClawConfig } from "../config/types.openclaw.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
@@ -21,6 +18,47 @@ import type { HealthCheck, HealthFinding } from "./health-checks.js";
const BROWSER_CLAWD_PROFILE_RESIDUE_CHECK_ID = "core/doctor/browser-clawd-profile-residue";
const FINAL_CONFIG_VALIDATION_CHECK_ID = "core/doctor/final-config-validation";
export type CoreHealthCheckDeps = {
readonly detectUnavailableSkills: (cfg: OpenClawConfig) => Promise<readonly SkillStatusEntry[]>;
readonly collectSecurityWarnings: (cfg: OpenClawConfig) => Promise<readonly string[]>;
readonly collectWorkspaceSuggestionNotes: (workspaceDir: string) => Promise<readonly string[]>;
};
async function detectUnavailableSkillsWithRuntime(
cfg: OpenClawConfig,
): Promise<readonly SkillStatusEntry[]> {
const runtime = await import("./doctor-core-checks.runtime.js");
return runtime.detectUnavailableSkills(cfg);
}
async function collectSecurityWarningsWithRuntime(cfg: OpenClawConfig): Promise<readonly string[]> {
const { collectSecurityWarnings } = await import("../commands/doctor-security.js");
return collectSecurityWarnings(cfg);
}
async function collectWorkspaceSuggestionNotesWithRuntime(
workspaceDir: string,
): Promise<readonly string[]> {
const { collectWorkspaceBackupTip } = await import("../commands/doctor-state-integrity.js");
const { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } =
await import("../commands/doctor-workspace.js");
const notes: string[] = [];
const backupTip = collectWorkspaceBackupTip(workspaceDir);
if (backupTip) {
notes.push(backupTip);
}
if (await shouldSuggestMemorySystem(workspaceDir)) {
notes.push(MEMORY_SYSTEM_PROMPT);
}
return notes;
}
const defaultCoreHealthCheckDeps: CoreHealthCheckDeps = {
detectUnavailableSkills: detectUnavailableSkillsWithRuntime,
collectSecurityWarnings: collectSecurityWarningsWithRuntime,
collectWorkspaceSuggestionNotes: collectWorkspaceSuggestionNotesWithRuntime,
};
export function configValidationIssuesToHealthFindings(
issues: readonly ConfigValidationIssue[],
): readonly HealthFinding[] {
@@ -385,23 +423,24 @@ const claudeCliCheck: HealthCheck = {
},
};
const securityCheck: HealthCheck = {
id: "core/doctor/security",
kind: "core",
description: "Security posture checks produce structured findings.",
source: "doctor",
async detect(ctx) {
const { collectSecurityWarnings } = await import("../commands/doctor-security.js");
const warnings = await collectSecurityWarnings(ctx.cfg);
return warnings.map((warning) =>
noteTextToFinding({
checkId: "core/doctor/security",
severity: warning.includes("CRITICAL") ? "error" : "warning",
text: warning,
}),
);
},
};
function createSecurityCheck(deps: CoreHealthCheckDeps): HealthCheck {
return {
id: "core/doctor/security",
kind: "core",
description: "Security posture checks produce structured findings.",
source: "doctor",
async detect(ctx) {
const warnings = await deps.collectSecurityWarnings(ctx.cfg);
return warnings.map((warning) =>
noteTextToFinding({
checkId: "core/doctor/security",
severity: warning.includes("CRITICAL") ? "error" : "warning",
text: warning,
}),
);
},
};
}
const openAIOAuthTlsCheck: HealthCheck = {
id: "core/doctor/oauth-tls",
@@ -513,39 +552,41 @@ const workspaceStatusCheck: HealthCheck = {
},
};
const skillsReadinessCheck: HealthCheck = {
id: "core/doctor/skills-readiness",
kind: "core",
description: "Allowed skills are usable in the current runtime environment.",
source: "doctor",
async detect(ctx, scope) {
const unavailable = filterUnavailableSkillsForScope(
detectUnavailableSkills(ctx.cfg),
scope?.paths,
);
return unavailable.map(unavailableSkillToFinding);
},
async repair(ctx, findings) {
const unavailable = filterUnavailableSkillsForScope(
detectUnavailableSkills(ctx.cfg),
findings.map((finding) => finding.path),
);
if (unavailable.length === 0) {
return { changes: [] };
}
const nextConfig = disableUnavailableSkillsInConfig(ctx.cfg, unavailable);
return {
config: nextConfig,
changes: unavailable.map((skill) => `Disabled unavailable skill ${skill.name}.`),
effects: unavailable.map((skill) => ({
kind: "config" as const,
action: ctx.dryRun === true ? "would-disable-skill" : "disable-skill",
target: skillReadinessPath(skill),
dryRunSafe: true,
})),
};
},
};
function createSkillsReadinessCheck(deps: CoreHealthCheckDeps): HealthCheck {
return {
id: "core/doctor/skills-readiness",
kind: "core",
description: "Allowed skills are usable in the current runtime environment.",
source: "doctor",
async detect(ctx, scope) {
const unavailable = filterUnavailableSkillsForScope(
await deps.detectUnavailableSkills(ctx.cfg),
scope?.paths,
);
return unavailable.map(unavailableSkillToFinding);
},
async repair(ctx, findings) {
const unavailable = filterUnavailableSkillsForScope(
await deps.detectUnavailableSkills(ctx.cfg),
findings.map((finding) => finding.path),
);
if (unavailable.length === 0) {
return { changes: [] };
}
const nextConfig = disableUnavailableSkillsInConfig(ctx.cfg, unavailable);
return {
config: nextConfig,
changes: unavailable.map((skill) => `Disabled unavailable skill ${skill.name}.`),
effects: unavailable.map((skill) => ({
kind: "config" as const,
action: ctx.dryRun === true ? "would-disable-skill" : "disable-skill",
target: skillReadinessPath(skill),
dryRunSafe: true,
})),
};
},
};
}
function unavailableSkillToFinding(skill: SkillStatusEntry): HealthFinding {
return {
@@ -674,54 +715,42 @@ const finalConfigValidationCheck: HealthCheck = {
},
};
const workspaceSuggestionsCheck: HealthCheck = {
id: "core/doctor/workspace-suggestions",
kind: "core",
description:
"Workspace backup and memory-system suggestions are captured as structured findings.",
source: "doctor",
async detect(ctx) {
const { collectWorkspaceBackupTip } = await import("../commands/doctor-state-integrity.js");
const { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } =
await import("../commands/doctor-workspace.js");
const workspaceDir = resolveAgentWorkspaceDir(ctx.cfg, resolveDefaultAgentId(ctx.cfg));
const findings: HealthFinding[] = [];
const backupTip = collectWorkspaceBackupTip(workspaceDir);
if (backupTip) {
findings.push(
function createWorkspaceSuggestionsCheck(deps: CoreHealthCheckDeps): HealthCheck {
return {
id: "core/doctor/workspace-suggestions",
kind: "core",
description:
"Workspace backup and memory-system suggestions are captured as structured findings.",
source: "doctor",
async detect(ctx) {
const workspaceDir = resolveAgentWorkspaceDir(ctx.cfg, resolveDefaultAgentId(ctx.cfg));
const notes = await deps.collectWorkspaceSuggestionNotes(workspaceDir);
return notes.map((text) =>
noteTextToFinding({
checkId: "core/doctor/workspace-suggestions",
severity: "info",
text: backupTip,
text,
}),
);
}
if (await shouldSuggestMemorySystem(workspaceDir)) {
findings.push(
noteTextToFinding({
checkId: "core/doctor/workspace-suggestions",
severity: "info",
text: MEMORY_SYSTEM_PROMPT,
}),
);
}
return findings;
},
};
},
};
}
const convertedWorkflowChecks: readonly HealthCheck[] = [
claudeCliCheck,
gatewayAuthCheck,
legacyStateCheck,
legacyWhatsAppCrontabCheck,
gatewayPlatformNotesCheck,
securityCheck,
browserCheck,
openAIOAuthTlsCheck,
hooksModelCheck,
bootstrapSizeCheck,
workspaceSuggestionsCheck,
];
function createConvertedWorkflowChecks(deps: CoreHealthCheckDeps): readonly HealthCheck[] {
return [
claudeCliCheck,
gatewayAuthCheck,
legacyStateCheck,
legacyWhatsAppCrontabCheck,
gatewayPlatformNotesCheck,
createSecurityCheck(deps),
browserCheck,
openAIOAuthTlsCheck,
hooksModelCheck,
bootstrapSizeCheck,
createWorkspaceSuggestionsCheck(deps),
];
}
let registered = false;
@@ -729,15 +758,9 @@ export function registerCoreHealthChecks(): void {
if (registered) {
return;
}
registerHealthCheck(gatewayConfigCheck);
for (const check of convertedWorkflowChecks) {
for (const check of CORE_HEALTH_CHECKS) {
registerHealthCheck(check);
}
registerHealthCheck(commandOwnerCheck);
registerHealthCheck(workspaceStatusCheck);
registerHealthCheck(skillsReadinessCheck);
registerHealthCheck(browserClawdProfileResidueCheck);
registerHealthCheck(finalConfigValidationCheck);
registered = true;
}
@@ -745,26 +768,22 @@ export function resetCoreHealthChecksForTest(): void {
registered = false;
}
export const CORE_HEALTH_CHECKS: readonly HealthCheck[] = [
gatewayConfigCheck,
...convertedWorkflowChecks,
commandOwnerCheck,
workspaceStatusCheck,
skillsReadinessCheck,
browserClawdProfileResidueCheck,
finalConfigValidationCheck,
];
function detectUnavailableSkills(cfg: OpenClawConfig): SkillStatusEntry[] {
const agentId = resolveDefaultAgentId(cfg);
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const report = buildWorkspaceSkillStatus(workspaceDir, {
config: cfg,
agentId,
});
return collectUnavailableAgentSkills(report);
export function createCoreHealthChecks(
deps: CoreHealthCheckDeps = defaultCoreHealthCheckDeps,
): readonly HealthCheck[] {
return [
gatewayConfigCheck,
...createConvertedWorkflowChecks(deps),
commandOwnerCheck,
workspaceStatusCheck,
createSkillsReadinessCheck(deps),
browserClawdProfileResidueCheck,
finalConfigValidationCheck,
];
}
export const CORE_HEALTH_CHECKS: readonly HealthCheck[] = createCoreHealthChecks();
function formatMissingSkillSummary(skill: SkillStatusEntry): string {
const missing: string[] = [];
if (skill.missing.bins.length > 0) {