Skip to content

Commit 2585249

Browse files
authored
perf: isolate doctor core check tests (#84493)
Merged via squash. Prepared head SHA: 6229656 Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com> Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com> Reviewed-by: @frankekn
1 parent 3d3cf96 commit 2585249

7 files changed

Lines changed: 454 additions & 211 deletions

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
Docs: https://docs.openclaw.ai
44

5+
## Unreleased
6+
7+
### Changes
8+
9+
- 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.
10+
511
## 2026.5.20
612

713
### Changes
@@ -68,7 +74,6 @@ Docs: https://docs.openclaw.ai
6874
- 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.
6975
- 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.
7076
- 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.
71-
7277
- 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.
7378
- 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.
7479
- Agents/subagents: constrain wildcard subagent target allowlists to configured agents while preserving explicitly listed compatibility targets. Fixes #84040. (#84357) Thanks @joshavant.

src/commands/doctor-skills-core.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
2+
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
4+
export function collectUnavailableAgentSkills(report: SkillStatusReport): SkillStatusEntry[] {
5+
return report.skills.filter(
6+
(skill) =>
7+
!skill.eligible &&
8+
!skill.disabled &&
9+
!skill.blockedByAllowlist &&
10+
!skill.blockedByAgentFilter,
11+
);
12+
}
13+
14+
export function disableUnavailableSkillsInConfig(
15+
config: OpenClawConfig,
16+
skills: readonly SkillStatusEntry[],
17+
): OpenClawConfig {
18+
if (skills.length === 0) {
19+
return config;
20+
}
21+
const entries = { ...config.skills?.entries };
22+
for (const skill of skills) {
23+
entries[skill.skillKey] = {
24+
...entries[skill.skillKey],
25+
enabled: false,
26+
};
27+
}
28+
return {
29+
...config,
30+
skills: {
31+
...config.skills,
32+
entries,
33+
},
34+
};
35+
}

src/commands/doctor-skills.ts

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { existsSync } from "node:fs";
22
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
3-
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
3+
import type { SkillStatusEntry } from "../agents/skills-status.js";
44
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
55
import {
66
detectGhConfigDirMismatch,
@@ -12,16 +12,15 @@ import { formatCliCommand } from "../cli/command-format.js";
1212
import type { OpenClawConfig } from "../config/types.openclaw.js";
1313
import { note } from "../terminal/note.js";
1414
import type { DoctorPrompter } from "./doctor-prompter.js";
15+
import {
16+
collectUnavailableAgentSkills,
17+
disableUnavailableSkillsInConfig,
18+
} from "./doctor-skills-core.js";
1519

16-
export function collectUnavailableAgentSkills(report: SkillStatusReport): SkillStatusEntry[] {
17-
return report.skills.filter(
18-
(skill) =>
19-
!skill.eligible &&
20-
!skill.disabled &&
21-
!skill.blockedByAllowlist &&
22-
!skill.blockedByAgentFilter,
23-
);
24-
}
20+
export {
21+
collectUnavailableAgentSkills,
22+
disableUnavailableSkillsInConfig,
23+
} from "./doctor-skills-core.js";
2524

2625
function formatMissingSummary(skill: SkillStatusEntry): string {
2726
const missing: string[] = [];
@@ -100,29 +99,6 @@ export function formatUnavailableSkillDoctorLines(skills: SkillStatusEntry[]): s
10099
return lines;
101100
}
102101

103-
export function disableUnavailableSkillsInConfig(
104-
config: OpenClawConfig,
105-
skills: readonly SkillStatusEntry[],
106-
): OpenClawConfig {
107-
if (skills.length === 0) {
108-
return config;
109-
}
110-
const entries = { ...config.skills?.entries };
111-
for (const skill of skills) {
112-
entries[skill.skillKey] = {
113-
...entries[skill.skillKey],
114-
enabled: false,
115-
};
116-
}
117-
return {
118-
...config,
119-
skills: {
120-
...config.skills,
121-
entries,
122-
},
123-
};
124-
}
125-
126102
export async function maybeRepairSkillReadiness(params: {
127103
cfg: OpenClawConfig;
128104
prompter: DoctorPrompter;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { promises as fs } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import type { OpenClawConfig } from "../config/types.openclaw.js";
6+
import { CORE_HEALTH_CHECKS } from "./doctor-core-checks.js";
7+
import type { HealthCheck } from "./health-checks.js";
8+
9+
const runtime = { log() {}, error() {}, exit() {} };
10+
11+
function getCheck(id: string): HealthCheck {
12+
const check = CORE_HEALTH_CHECKS.find((entry) => entry.id === id);
13+
if (!check) {
14+
throw new Error(`Missing health check ${id}`);
15+
}
16+
return check;
17+
}
18+
19+
describe("doctor core skills readiness smoke", () => {
20+
let tmp: string | undefined;
21+
22+
afterEach(async () => {
23+
if (tmp !== undefined) {
24+
await fs.rm(tmp, { recursive: true, force: true });
25+
tmp = undefined;
26+
}
27+
});
28+
29+
it("detects and repairs a real unavailable workspace skill", async () => {
30+
tmp = await fs.mkdtemp(join(tmpdir(), "openclaw-health-skills-"));
31+
const skillDir = join(tmp, "skills", "missing-tool");
32+
await fs.mkdir(skillDir, { recursive: true });
33+
await fs.writeFile(
34+
join(skillDir, "SKILL.md"),
35+
`---
36+
name: missing-tool
37+
description: Missing tool
38+
metadata: '{"openclaw":{"requires":{"bins":["openclaw-test-missing-skill-bin"]}}}'
39+
---
40+
41+
# Missing tool
42+
`,
43+
"utf-8",
44+
);
45+
const cfg: OpenClawConfig = {
46+
agents: {
47+
defaults: {
48+
workspace: tmp,
49+
skills: ["missing-tool"],
50+
},
51+
},
52+
};
53+
const check = getCheck("core/doctor/skills-readiness");
54+
55+
const findings = await check.detect({
56+
mode: "lint",
57+
runtime,
58+
cfg,
59+
cwd: tmp,
60+
});
61+
expect(findings).toContainEqual(
62+
expect.objectContaining({
63+
checkId: "core/doctor/skills-readiness",
64+
severity: "warning",
65+
path: "skills.entries.missing-tool.enabled",
66+
}),
67+
);
68+
await expect(
69+
check.detect(
70+
{
71+
mode: "fix",
72+
runtime,
73+
cfg,
74+
cwd: tmp,
75+
},
76+
{ paths: ["skills.entries.other-tool.enabled"] },
77+
),
78+
).resolves.toEqual([]);
79+
await expect(
80+
check.detect(
81+
{
82+
mode: "fix",
83+
runtime,
84+
cfg,
85+
cwd: tmp,
86+
},
87+
{ paths: ["skills.entries.missing-tool.enabled"] },
88+
),
89+
).resolves.toContainEqual(
90+
expect.objectContaining({
91+
path: "skills.entries.missing-tool.enabled",
92+
}),
93+
);
94+
95+
const repaired = await check.repair?.(
96+
{
97+
mode: "fix",
98+
runtime,
99+
cfg,
100+
cwd: tmp,
101+
},
102+
findings,
103+
);
104+
expect(repaired?.config?.skills?.entries?.["missing-tool"]).toEqual({ enabled: false });
105+
expect(repaired?.changes).toContain("Disabled unavailable skill missing-tool.");
106+
expect(repaired?.effects).toContainEqual(
107+
expect.objectContaining({
108+
kind: "config",
109+
action: "disable-skill",
110+
target: "skills.entries.missing-tool.enabled",
111+
}),
112+
);
113+
});
114+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
2+
import { buildWorkspaceSkillStatus, type SkillStatusEntry } from "../agents/skills-status.js";
3+
import { collectUnavailableAgentSkills } from "../commands/doctor-skills-core.js";
4+
import type { OpenClawConfig } from "../config/types.openclaw.js";
5+
6+
export function detectUnavailableSkills(cfg: OpenClawConfig): SkillStatusEntry[] {
7+
const agentId = resolveDefaultAgentId(cfg);
8+
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
9+
const report = buildWorkspaceSkillStatus(workspaceDir, {
10+
config: cfg,
11+
agentId,
12+
});
13+
return collectUnavailableAgentSkills(report);
14+
}

0 commit comments

Comments
 (0)