diff --git a/Overlord-Server/src/server/plugin-state-bundle.test.ts b/Overlord-Server/src/server/plugin-state-bundle.test.ts new file mode 100644 index 0000000..4211ca5 --- /dev/null +++ b/Overlord-Server/src/server/plugin-state-bundle.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, mkdir, rm, writeFile } from "fs/promises"; +import { tmpdir } from "os"; +import { join } from "path"; +import { loadPluginBundle } from "./plugin-state-bundle"; + +let tempRoots: string[] = []; + +async function createTempRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), "overlord-plugin-bundle-test-")); + tempRoots.push(root); + return root; +} + +afterEach(async () => { + await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true }))); + tempRoots = []; +}); + +describe("loadPluginBundle", () => { + test("matches plugin binaries when clients report a pretty Windows OS name", async () => { + const root = await createTempRoot(); + const pluginDir = join(root, "sample-c"); + await mkdir(pluginDir, { recursive: true }); + await writeFile( + join(pluginDir, "manifest.json"), + JSON.stringify({ + id: "sample-c", + name: "sample-c", + binaries: { "windows-amd64": "sample-c-windows-amd64.dll" }, + }), + ); + await writeFile(join(pluginDir, "sample-c-windows-amd64.dll"), "test-binary"); + + const bundle = await loadPluginBundle(root, "sample-c", async () => {}, "Windows 11 Pro 24H2", "amd64"); + + expect(bundle.binaryPath).toBe(join(pluginDir, "sample-c-windows-amd64.dll")); + expect(bundle.size).toBe("test-binary".length); + }); +}); diff --git a/Overlord-Server/src/server/plugin-state-bundle.ts b/Overlord-Server/src/server/plugin-state-bundle.ts index 0843348..4d68e43 100644 --- a/Overlord-Server/src/server/plugin-state-bundle.ts +++ b/Overlord-Server/src/server/plugin-state-bundle.ts @@ -262,8 +262,9 @@ export async function loadPluginBundle( let binaryPath: string | null = null; if (manifest.binaries && clientOS && clientArch) { - const key = `${clientOS}-${clientArch}`; - const filename = manifest.binaries[key]; + const filename = getPluginBinaryCandidateKeys(clientOS, clientArch) + .map((key) => manifest.binaries?.[key]) + .find((name): name is string => typeof name === "string" && name.length > 0); if (filename) { const candidate = path.join(dir, filename); try { @@ -274,7 +275,9 @@ export async function loadPluginBundle( } if (!binaryPath) { - const platformKey = clientOS && clientArch ? `${clientOS}-${clientArch}` : "unknown"; + const platformKey = clientOS && clientArch ? formatPluginPlatformKey(clientOS, clientArch) : "unknown"; + const normalizedOS = normalizePluginOS(clientOS); + const normalizedArch = normalizePluginArch(clientArch); const files = await fs.readdir(dir); const archRegex = /-(linux|darwin|windows|freebsd)-(amd64|arm64|arm|386)\.(so|dll|dylib)$/i; @@ -282,8 +285,8 @@ export async function loadPluginBundle( const m = f.match(archRegex); if (m) { return ( - m[1].toLowerCase() === (clientOS || "").toLowerCase() && - m[2].toLowerCase() === (clientArch || "").toLowerCase() + m[1].toLowerCase() === normalizedOS && + m[2].toLowerCase() === normalizedArch ); } const fl = f.toLowerCase(); @@ -301,6 +304,39 @@ export async function loadPluginBundle( return { manifest, binaryPath, size: stat.size }; } +function getPluginBinaryCandidateKeys(clientOS?: string, clientArch?: string): string[] { + if (!clientOS || !clientArch) return []; + const rawKey = `${clientOS}-${clientArch}`.toLowerCase(); + const normalizedKey = formatPluginPlatformKey(clientOS, clientArch); + return Array.from( + new Set([normalizedKey, rawKey].filter((key) => !key.startsWith("unknown-") && !key.endsWith("-unknown"))), + ); +} + +function formatPluginPlatformKey(clientOS?: string, clientArch?: string): string { + return `${normalizePluginOS(clientOS)}-${normalizePluginArch(clientArch)}`; +} + +function normalizePluginOS(value?: string): string { + const os = (value || "").trim().toLowerCase(); + if (!os) return "unknown"; + if (os.includes("windows") || os === "win32") return "windows"; + if (os.includes("mac os") || os.includes("macos") || os.includes("os x") || os === "darwin") return "darwin"; + if (os.includes("linux")) return "linux"; + if (os.includes("freebsd")) return "freebsd"; + return os; +} + +function normalizePluginArch(value?: string): string { + const arch = (value || "").trim().toLowerCase(); + if (!arch) return "unknown"; + if (arch === "x64" || arch === "x86_64") return "amd64"; + if (arch === "ia32" || arch === "i386" || arch === "x86") return "386"; + if (arch === "aarch64") return "arm64"; + if (arch === "armv7" || arch === "armv6") return "arm"; + return arch; +} + export type PluginPull = { id: string; clientId: string;