openpetswithchatandmcp/apps/desktop/tests/plugin-package.test.ts
OpenPets Dev 6ab3bb64d8 feat(rebrand): rename OpenPets to FamiliarOS and pets to familiars
- Rename all user-facing and technical identifiers from OpenPets/Pet to FamiliarOS/Familiar.
- Rename packages from @open-pets/* to @familiaros/*; rename install-pet/pet-format packages.
- Rename plugin IDs and directories from openpets.* to familiaros.*.
- Rename IPC namespace from openpets:* to familiaros:* and state filenames from openpets-* to familiaros-* with legacy migration.
- Rename source files (pet-window, built-in-pet, default-pet-controller, etc.) to familiar equivalents.
- Update locales (en, es-419, ja, ko, pt-BR, zh-Hans, zh-Hant) and tray/pet context menu strings.
- Add Familiar naming feature: preference, settings input, tray menu display.
- Update assets and packaging config; all desktop tests pass.
2026-06-17 01:42:08 +00:00

66 lines
6.6 KiB
TypeScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdtempSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { validatePluginCatalog } from "../src/plugin-catalog-validation.js";
import { installCatalogPluginPackage, safeDeletePluginInstallDir, validatePluginZipUrl } from "../src/plugin-package.js";
import type { FamiliarOSPluginManifest } from "../src/plugin-manifest.js";
const manifest: FamiliarOSPluginManifest = { manifestVersion: 1, id: "zip-plug", name: "Zip Plug", version: "1.0.0", runtime: "declarative", permissions: ["timer", "familiar:speak"], triggers: [{ on: "timer", everyMinutes: 5, actions: [{ type: "familiar.speak", message: "hi" }] }] };
const text = JSON.stringify(manifest);
const zip = makeZip("familiaros.plugin.json", Buffer.from(text));
const entry = { id: manifest.id, name: manifest.name, version: manifest.version, description: "desc", runtime: "declarative" as const, permissions: manifest.permissions, downloadUrl: "https://zip.familiaros.dev/plugins/zip-plug.zip", sha256: createHash("sha256").update(zip).digest("hex") };
validatePluginZipUrl(entry.downloadUrl);
assert.throws(() => validatePluginZipUrl("http://zip.familiaros.dev/plugins/x.zip"), /not allowed/);
assert.throws(() => validatePluginZipUrl("https://zip.familiaros.dev:444/plugins/x.zip"), /not allowed/);
const userData = mkdtempSync(join(tmpdir(), "familiaros-plugin-package-"));
const installed = await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: entry, zip });
assert.equal(installed.manifest.id, manifest.id);
assert.equal(readFileSync(join(userData, "plugins", manifest.id, "familiaros.plugin.json"), "utf8"), text);
assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: { ...entry, name: "Other" }, zip }), /does not match/);
assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: entry, zip: makeZip("nested/familiaros.plugin.json", Buffer.from(text)) }), /invalid|exactly one root manifest|must contain familiaros/);
const jsManifest: FamiliarOSPluginManifest = { manifestVersion: 2, id: "js-plug", name: "JS Plug", version: "1.0.0", runtime: "javascript", sdkVersion: "1.0.0", entry: "index.mjs", permissions: ["familiar:speak"] };
const jsText = JSON.stringify(jsManifest);
const jsEntryText = "export default {};\n";
const jsZip = makeZipFiles([{ name: "familiaros.plugin.json", data: Buffer.from(jsText) }, { name: "index.mjs", data: Buffer.from(jsEntryText) }]);
const jsCatalogEntry = { id: jsManifest.id, name: jsManifest.name, version: jsManifest.version, description: "desc", runtime: "javascript" as const, sdkVersion: "1.0.0", permissions: jsManifest.permissions, downloadUrl: "https://zip.familiaros.dev/plugins/js-plug.zip", sha256: createHash("sha256").update(jsZip).digest("hex") };
const jsInstalled = await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: jsCatalogEntry, zip: jsZip });
assert.equal(readFileSync(join(jsInstalled.installPath, "index.mjs"), "utf8"), jsEntryText);
await assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: jsCatalogEntry, zip: makeZipFiles([{ name: "familiaros.plugin.json", data: Buffer.from(jsText) }]) }), /manifest and entry/);
await assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: jsCatalogEntry, zip: makeZipFiles([{ name: "familiaros.plugin.json", data: Buffer.from(jsText) }, { name: "index.mjs", data: Buffer.from(jsEntryText) }, { name: "extra.js", data: Buffer.from("") }]) }), /manifest and entry|too many entries/);
const canonicalEntry = validatePluginCatalog({ version: 1, generatedAt: new Date().toISOString(), plugins: [{ ...entry, permissions: ["timer", "familiar:speak"] }] }).plugins[0];
await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: canonicalEntry, zip });
const oldText = JSON.stringify({ ...manifest, version: "0.9.0" });
writeFileSync(join(userData, "plugins", manifest.id, "familiaros.plugin.json"), oldText, "utf8");
await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: entry, zip });
assert.equal(readFileSync(join(userData, "plugins", manifest.id, "familiaros.plugin.json"), "utf8"), text);
const deleteRoot = mkdtempSync(join(tmpdir(), "familiaros-plugin-delete-"));
const outside = mkdtempSync(join(tmpdir(), "familiaros-plugin-outside-"));
symlinkSync(outside, join(deleteRoot, "plugins"), "dir");
await assert.rejects(() => safeDeletePluginInstallDir(deleteRoot, manifest.id, join(deleteRoot, "plugins", manifest.id), "catalog"), /invalid|unexpected/);
console.error("Plugin package validation passed.");
function makeZip(name: string, data: Buffer): Buffer {
return makeZipFiles([{ name, data }]);
}
function makeZipFiles(files: Array<{ name: string; data: Buffer }>): Buffer {
const locals: Buffer[] = []; const centrals: Buffer[] = []; let offset = 0;
for (const file of files) {
const nameBuffer = Buffer.from(file.name); const crc = crc32(file.data); const now = 0;
const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0, 6); local.writeUInt16LE(0, 8); local.writeUInt32LE(now, 10); local.writeUInt32LE(crc, 14); local.writeUInt32LE(file.data.length, 18); local.writeUInt32LE(file.data.length, 22); local.writeUInt16LE(nameBuffer.length, 26);
const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0, 8); central.writeUInt16LE(0, 10); central.writeUInt32LE(now, 12); central.writeUInt32LE(crc, 16); central.writeUInt32LE(file.data.length, 20); central.writeUInt32LE(file.data.length, 24); central.writeUInt16LE(nameBuffer.length, 28); central.writeUInt32LE((0o100644 << 16) >>> 0, 38); central.writeUInt32LE(offset, 42);
const localPart = Buffer.concat([local, nameBuffer, file.data]); locals.push(localPart); centrals.push(Buffer.concat([central, nameBuffer])); offset += localPart.length;
}
const localPart = Buffer.concat(locals); const centralPart = Buffer.concat(centrals); const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(files.length, 8); end.writeUInt16LE(files.length, 10); end.writeUInt32LE(centralPart.length, 12); end.writeUInt32LE(localPart.length, 16); return Buffer.concat([localPart, centralPart, end]);
}
function crc32(buffer: Buffer): number { let crc = -1; for (const byte of buffer) { crc ^= byte; for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); } return (crc ^ -1) >>> 0; }