mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
* fix(ci): include live provider plugins in shared Docker images * fix(ci): register the live Docker selector entrypoint
168 lines
6.3 KiB
TypeScript
168 lines
6.3 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { resolveDockerPluginSelection } from "../../scripts/lib/docker-plugin-selection.mjs";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const repoRoot = path.resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
const selectorScript = path.join(repoRoot, "scripts/lib/docker-plugin-selection.mjs");
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
function writePlugin(
|
|
extensionsRoot: string,
|
|
dirName: string,
|
|
manifestId?: string,
|
|
dependencies?: Record<string, string>,
|
|
requiredPlatformPackages?: string[],
|
|
) {
|
|
const pluginDir = path.join(extensionsRoot, dirName);
|
|
fs.mkdirSync(pluginDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(pluginDir, "package.json"),
|
|
`${JSON.stringify({
|
|
name: dirName,
|
|
...(dependencies && { dependencies }),
|
|
...(requiredPlatformPackages && {
|
|
openclaw: { install: { requiredPlatformPackages } },
|
|
}),
|
|
})}\n`,
|
|
);
|
|
if (manifestId) {
|
|
fs.writeFileSync(
|
|
path.join(pluginDir, "openclaw.plugin.json"),
|
|
`${JSON.stringify({ id: manifestId })}\n`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function runSelector(
|
|
extensionsRoot: string,
|
|
selection: string,
|
|
rootPackagePath?: string,
|
|
requiredPlatformPackages = false,
|
|
) {
|
|
const args = [selectorScript, extensionsRoot, selection];
|
|
if (rootPackagePath) {
|
|
args.push("--required-bundled", rootPackagePath);
|
|
} else if (requiredPlatformPackages) {
|
|
args.push("--required-platform-packages");
|
|
}
|
|
return spawnSync(process.execPath, args, {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
|
|
describe("Docker plugin selection", () => {
|
|
it("adds standalone direct and Gateway provider selections to the shared live image", () => {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.join(repoRoot, "scripts/print-live-docker-plugin-selection.mjs"), repoRoot, "twitch"],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
OPENCLAW_LIVE_PROVIDERS: "ollama",
|
|
OPENCLAW_LIVE_GATEWAY_MODELS: "mistral/mistral-large-latest",
|
|
},
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout.trim().split(",")).toEqual(
|
|
expect.arrayContaining(["ollama", "mistral", "twitch"]),
|
|
);
|
|
});
|
|
|
|
it("selects provider owners by manifest capability without assuming matching plugin ids", () => {
|
|
const extensionsRoot = tempDirs.make("openclaw-docker-provider-selection-");
|
|
writePlugin(extensionsRoot, "provider-source", "provider-plugin");
|
|
writePlugin(extensionsRoot, "other-source", "other-plugin");
|
|
fs.writeFileSync(
|
|
path.join(extensionsRoot, "provider-source", "openclaw.plugin.json"),
|
|
JSON.stringify({ id: "provider-plugin", providers: ["api-provider", "portal-provider"] }),
|
|
);
|
|
|
|
expect(
|
|
resolveDockerPluginSelection({
|
|
extensionsRoot,
|
|
selection: "other-plugin,provider-plugin",
|
|
providers: ["portal-provider", "custom-unregistered-provider"],
|
|
}),
|
|
).toEqual(["other-source", "provider-source"]);
|
|
expect(resolveDockerPluginSelection({ extensionsRoot, providers: ["api-provider"] })).toEqual([
|
|
"provider-source",
|
|
]);
|
|
});
|
|
|
|
it("includes required core-bundled dependencies without changing optional plugin selections", () => {
|
|
const fixtureRoot = tempDirs.make("openclaw-docker-required-bundled-plugins-");
|
|
const extensionsRoot = path.join(fixtureRoot, "extensions");
|
|
const rootPackagePath = path.join(fixtureRoot, "package.json");
|
|
fs.mkdirSync(extensionsRoot);
|
|
fs.writeFileSync(
|
|
rootPackagePath,
|
|
`${JSON.stringify({ files: ["dist/", "!dist/extensions/optional-provider/**"] })}\n`,
|
|
);
|
|
writePlugin(extensionsRoot, "bundled-provider", "bundled", { "provider-sdk": "1.0.0" });
|
|
writePlugin(extensionsRoot, "bundled-without-deps", "without-deps");
|
|
writePlugin(extensionsRoot, "optional-provider", "optional", { "optional-sdk": "1.0.0" }, [
|
|
"optional-native-linux-arm64",
|
|
"optional-native-darwin-arm64",
|
|
"optional-native-linux-arm64",
|
|
]);
|
|
|
|
const required = runSelector(extensionsRoot, "", rootPackagePath);
|
|
expect(required.status).toBe(0);
|
|
expect(required.stderr).toBe("");
|
|
expect(required.stdout).toBe("bundled-provider\n");
|
|
|
|
const install = runSelector(extensionsRoot, "optional", rootPackagePath);
|
|
expect(install.status).toBe(0);
|
|
expect(install.stdout).toBe("bundled-provider\noptional-provider\n");
|
|
|
|
const selected = runSelector(extensionsRoot, "optional");
|
|
expect(selected.status).toBe(0);
|
|
expect(selected.stdout).toBe("optional-provider\n");
|
|
|
|
const platformPackages = runSelector(extensionsRoot, "optional", undefined, true);
|
|
expect(platformPackages.status).toBe(0);
|
|
expect(platformPackages.stdout).toBe(
|
|
"optional-native-darwin-arm64\noptional-native-linux-arm64\n",
|
|
);
|
|
expect(runSelector(extensionsRoot, "", undefined, true).stdout).toBe("");
|
|
});
|
|
|
|
it("resolves manifest ids and source directory names deterministically", () => {
|
|
const extensionsRoot = tempDirs.make("openclaw-docker-plugin-selection-");
|
|
writePlugin(extensionsRoot, "source-only");
|
|
writePlugin(extensionsRoot, "provider-source", "provider-id");
|
|
|
|
const result = runSelector(
|
|
extensionsRoot,
|
|
"source-only,provider-id provider-source,provider-id",
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(result.stdout).toBe("provider-source\nsource-only\n");
|
|
});
|
|
|
|
it("fails closed for unknown, invalid, and ambiguous ids", () => {
|
|
const extensionsRoot = tempDirs.make("openclaw-docker-plugin-selection-errors-");
|
|
writePlugin(extensionsRoot, "shared");
|
|
writePlugin(extensionsRoot, "other-source", "shared");
|
|
|
|
for (const [selection, message] of [
|
|
["missing-plugin", "unknown OPENCLAW_EXTENSIONS plugin id: missing-plugin"],
|
|
["../invalid", "invalid OPENCLAW_EXTENSIONS plugin id: ../invalid"],
|
|
["shared", "ambiguous OPENCLAW_EXTENSIONS plugin id: shared"],
|
|
] as const) {
|
|
const result = runSelector(extensionsRoot, selection);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toContain(message);
|
|
}
|
|
});
|
|
});
|