Files
openclaw/extensions/canvas/doctor-contract-api.test.ts
Peter Steinberger ae55a4090c refactor(canvas): make the panel a widget presenter (#126030)
* refactor(canvas): retire legacy host and commands

* refactor(apple): narrow shared Canvas contracts

* refactor(macos): keep Canvas as widget presenter

* refactor(ios): remove Canvas client

* refactor(android): remove Canvas client

* refactor(linux): remove Canvas client

* fix(ci): isolate native locale artifacts

* fix(linux): regenerate companion lockfile

* fix(canvas): refresh native tool display metadata

* test(canvas): align coverage with presenter surface

* test(canvas): remove obsolete asset root seam

* test(canvas): stabilize retirement CI coverage

* refactor(swift): remove orphaned resource wrapper

* test(ios): remove retired canvas layout assertion

* fix(macos): reserve retired canvas command namespace

* refactor(macos): isolate canvas command policy

* fix(canvas): select only eligible macOS panels

* fix(canvas): keep panel selection plugin-owned
2026-08-19 08:21:07 -07:00

205 lines
8.0 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { PluginDoctorStateMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { resolvePreferredOpenClawTmpDir, tempWorkspace } from "openclaw/plugin-sdk/temp-path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
legacyConfigRules,
normalizeCompatibilityConfig,
stateMigrations,
} from "./doctor-contract-api.js";
const migration = stateMigrations[0];
const canvasDoctorWorkspaceRoot = resolvePreferredOpenClawTmpDir();
function createCanvasDoctorWorkspace(kind: "state" | "custom") {
return tempWorkspace({
rootDir: canvasDoctorWorkspaceRoot,
prefix: `openclaw-canvas-doctor-${kind}-`,
});
}
afterEach(() => {
vi.restoreAllMocks();
});
describe("Canvas doctor config repair", () => {
it("flags and removes retired file-host settings", () => {
expect(legacyConfigRules.map((rule) => rule.path)).toEqual([
["canvasHost"],
["plugins", "entries", "canvas", "config", "host", "root"],
["plugins", "entries", "canvas", "config", "host", "port"],
["plugins", "entries", "canvas", "config", "host", "liveReload"],
]);
expect(legacyConfigRules.every((rule) => rule.message.includes("doctor --fix"))).toBe(true);
const cfg = {
plugins: {
entries: {
canvas: {
enabled: true,
config: {
host: { enabled: false, root: "~/canvas", port: 18793, liveReload: true },
},
},
},
},
} as OpenClawConfig;
const result = normalizeCompatibilityConfig({ cfg });
expect(result.config.plugins?.entries?.canvas).toEqual({
enabled: true,
config: { host: { enabled: false } },
});
expect(result.changes).toEqual([
"Removed retired Canvas host config: plugins.entries.canvas.config.host.root, plugins.entries.canvas.config.host.port, plugins.entries.canvas.config.host.liveReload.",
]);
expect(cfg.plugins?.entries?.canvas?.config).toEqual({
host: { enabled: false, root: "~/canvas", port: 18793, liveReload: true },
});
expect(normalizeCompatibilityConfig({ cfg: result.config })).toEqual({
config: result.config,
changes: [],
});
});
});
function migrationParams(params: {
stateDir: string;
customRoot?: string;
}): Parameters<PluginDoctorStateMigration["detectLegacyState"]>[0] {
const config = params.customRoot
? ({
plugins: {
entries: {
canvas: { config: { host: { root: params.customRoot } } },
},
},
} as OpenClawConfig)
: ({} as OpenClawConfig);
return {
config,
env: process.env,
stateDir: params.stateDir,
oauthDir: path.join(params.stateDir, "credentials"),
context: { openPluginStateKeyedStore: () => undefined as never },
};
}
describe("Canvas doctor state migration", () => {
it("ignores the default core document root", async () => {
await using stateWorkspace = await createCanvasDoctorWorkspace("state");
const stateDir = stateWorkspace.dir;
await fs.mkdir(path.join(stateDir, "canvas", "documents", "cv_default"), {
recursive: true,
});
await expect(migration?.detectLegacyState(migrationParams({ stateDir }))).resolves.toBeNull();
});
it("moves custom-root documents into the stable core layout", async () => {
await using stateWorkspace = await createCanvasDoctorWorkspace("state");
await using customWorkspace = await createCanvasDoctorWorkspace("custom");
const stateDir = stateWorkspace.dir;
const customRoot = customWorkspace.dir;
const legacyDocumentDir = path.join(customRoot, "documents", "cv_existing");
await fs.mkdir(path.join(legacyDocumentDir, "collection.media"), { recursive: true });
await fs.writeFile(path.join(legacyDocumentDir, "index.html"), "<p>existing</p>", "utf8");
await fs.writeFile(
path.join(legacyDocumentDir, "collection.media", "asset.txt"),
"asset",
"utf8",
);
const params = migrationParams({ stateDir, customRoot });
await expect(migration?.detectLegacyState(params)).resolves.toEqual({
preview: [
`- Canvas documents: ${path.join(customRoot, "documents")} -> ${path.join(stateDir, "canvas", "documents")} (1 document(s))`,
],
});
const result = await migration?.migrateLegacyState(params);
expect(result).toEqual({
changes: ["Migrated 1 Canvas document(s) into core storage"],
warnings: [],
});
await expect(
fs.readFile(path.join(stateDir, "canvas", "documents", "cv_existing", "index.html"), "utf8"),
).resolves.toBe("<p>existing</p>");
await expect(
fs.readFile(
path.join(stateDir, "canvas", "documents", "cv_existing", "collection.media", "asset.txt"),
"utf8",
),
).resolves.toBe("asset");
await expect(fs.access(legacyDocumentDir)).rejects.toThrow();
});
it("leaves a conflicting legacy document in place", async () => {
await using stateWorkspace = await createCanvasDoctorWorkspace("state");
await using customWorkspace = await createCanvasDoctorWorkspace("custom");
const stateDir = stateWorkspace.dir;
const customRoot = customWorkspace.dir;
const legacyDocumentDir = path.join(customRoot, "documents", "cv_conflict");
const coreDocumentDir = path.join(stateDir, "canvas", "documents", "cv_conflict");
await fs.mkdir(legacyDocumentDir, { recursive: true });
await fs.mkdir(coreDocumentDir, { recursive: true });
await fs.writeFile(path.join(legacyDocumentDir, "index.html"), "legacy", "utf8");
await fs.writeFile(path.join(coreDocumentDir, "index.html"), "core", "utf8");
const result = await migration?.migrateLegacyState(migrationParams({ stateDir, customRoot }));
expect(result?.changes).toEqual([]);
expect(result?.warnings).toHaveLength(1);
await expect(fs.readFile(path.join(legacyDocumentDir, "index.html"), "utf8")).resolves.toBe(
"legacy",
);
await expect(fs.readFile(path.join(coreDocumentDir, "index.html"), "utf8")).resolves.toBe(
"core",
);
});
it("cleans partial copies and retries the migration", async () => {
await using stateWorkspace = await createCanvasDoctorWorkspace("state");
await using customWorkspace = await createCanvasDoctorWorkspace("custom");
const stateDir = stateWorkspace.dir;
const customRoot = customWorkspace.dir;
const legacyDocumentDir = path.join(customRoot, "documents", "cv_retry");
const coreDocumentsDir = path.join(stateDir, "canvas", "documents");
const coreDocumentDir = path.join(coreDocumentsDir, "cv_retry");
await fs.mkdir(legacyDocumentDir, { recursive: true });
await fs.writeFile(path.join(legacyDocumentDir, "index.html"), "complete", "utf8");
const params = migrationParams({ stateDir, customRoot });
const copy = vi.spyOn(fs, "cp").mockImplementationOnce(async (_source, destination) => {
const destinationPath =
typeof destination === "string" ? destination : fileURLToPath(destination);
await fs.mkdir(destinationPath, { recursive: true });
await fs.writeFile(path.join(destinationPath, "index.html"), "partial", "utf8");
throw new Error("interrupted copy");
});
const failed = await migration?.migrateLegacyState(params);
expect(failed?.changes).toEqual([]);
expect(failed?.warnings).toHaveLength(1);
await expect(fs.access(coreDocumentDir)).rejects.toThrow();
await expect(fs.readFile(path.join(legacyDocumentDir, "index.html"), "utf8")).resolves.toBe(
"complete",
);
expect(
(await fs.readdir(coreDocumentsDir)).filter((name) => name.startsWith(".canvas-migrate-")),
).toEqual([]);
copy.mockRestore();
await expect(migration?.migrateLegacyState(params)).resolves.toEqual({
changes: ["Migrated 1 Canvas document(s) into core storage"],
warnings: [],
});
await expect(fs.readFile(path.join(coreDocumentDir, "index.html"), "utf8")).resolves.toBe(
"complete",
);
});
});