
























@@ -0,0 +1,123 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, describe, expect, it } from "vitest";
5+import {
6+detectUiProtocolFreshnessIssues,
7+uiProtocolFreshnessIssueToHealthFinding,
8+uiProtocolFreshnessIssueToRepairEffects,
9+type UiProtocolFreshnessIssue,
10+} from "./doctor-ui.js";
11+12+const tempRoots: string[] = [];
13+14+function issue(overrides: Partial<UiProtocolFreshnessIssue> = {}): UiProtocolFreshnessIssue {
15+return {
16+kind: "missing-assets",
17+root: "/repo/openclaw",
18+uiIndexPath: "/repo/openclaw/dist/control-ui/index.html",
19+canBuild: true,
20+ ...overrides,
21+} as UiProtocolFreshnessIssue;
22+}
23+24+async function createOpenClawRoot(): Promise<string> {
25+const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-doctor-ui-"));
26+tempRoots.push(root);
27+await fs.writeFile(path.join(root, "package.json"), JSON.stringify({ name: "openclaw" }));
28+await fs.mkdir(path.join(root, "packages/gateway-protocol/src"), { recursive: true });
29+await fs.writeFile(path.join(root, "packages/gateway-protocol/src/schema.ts"), "export {};\n");
30+return root;
31+}
32+33+async function touch(filePath: string, date: Date): Promise<void> {
34+await fs.mkdir(path.dirname(filePath), { recursive: true });
35+await fs.writeFile(filePath, "");
36+await fs.utimes(filePath, date, date);
37+}
38+39+describe("UI protocol freshness health mapping", () => {
40+afterEach(async () => {
41+await Promise.all(
42+tempRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })),
43+);
44+});
45+46+it("maps missing UI assets to a structured finding and dry-run effect", () => {
47+const current = issue();
48+49+expect(uiProtocolFreshnessIssueToHealthFinding(current)).toEqual(
50+expect.objectContaining({
51+checkId: "core/doctor/ui-protocol-freshness",
52+severity: "warning",
53+path: "/repo/openclaw/dist/control-ui/index.html",
54+fixHint: expect.stringContaining("openclaw doctor --fix"),
55+}),
56+);
57+expect(uiProtocolFreshnessIssueToRepairEffects(current)).toEqual([
58+{
59+kind: "process",
60+action: "would-build-control-ui",
61+target: "/repo/openclaw",
62+dryRunSafe: false,
63+},
64+]);
65+});
66+67+it("maps stale UI assets to rebuild effects without file diffs", () => {
68+const current = issue({
69+kind: "stale-assets",
70+changesSinceBuild: ["abc123 schema change"],
71+});
72+const finding = uiProtocolFreshnessIssueToHealthFinding(current);
73+74+expect(finding.message).toContain("abc123 schema change");
75+expect(finding.fixHint).toContain("openclaw doctor --fix --force");
76+expect(uiProtocolFreshnessIssueToRepairEffects(current)).toEqual([
77+{
78+kind: "process",
79+action: "would-rebuild-control-ui",
80+target: "/repo/openclaw",
81+dryRunSafe: false,
82+},
83+]);
84+});
85+86+it("does not report dry-run effects when UI sources are unavailable", () => {
87+expect(uiProtocolFreshnessIssueToRepairEffects(issue({ canBuild: false }))).toEqual([]);
88+});
89+90+it("does not report stale assets when git finds no schema changes", async () => {
91+const root = await createOpenClawRoot();
92+const schemaPath = path.join(root, "packages/gateway-protocol/src/schema.ts");
93+const uiIndexPath = path.join(root, "dist/control-ui/index.html");
94+await touch(uiIndexPath, new Date("2026-01-01T00:00:00.000Z"));
95+await touch(schemaPath, new Date("2026-01-02T00:00:00.000Z"));
96+97+await expect(
98+detectUiProtocolFreshnessIssues({
99+ root,
100+async collectChangesSinceBuild() {
101+return [];
102+},
103+}),
104+).resolves.toEqual([]);
105+});
106+107+it("does not report stale assets when git history is unavailable", async () => {
108+const root = await createOpenClawRoot();
109+const schemaPath = path.join(root, "packages/gateway-protocol/src/schema.ts");
110+const uiIndexPath = path.join(root, "dist/control-ui/index.html");
111+await touch(uiIndexPath, new Date("2026-01-01T00:00:00.000Z"));
112+await touch(schemaPath, new Date("2026-01-02T00:00:00.000Z"));
113+114+await expect(
115+detectUiProtocolFreshnessIssues({
116+ root,
117+async collectChangesSinceBuild() {
118+return null;
119+},
120+}),
121+).resolves.toEqual([]);
122+});
123+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。