- 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.
152 lines
8.2 KiB
TypeScript
152 lines
8.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
import { initializePluginState, openPetsPluginStateFileName, PluginStateStore, type PluginStateRecord } from "../src/plugin-state.js";
|
|
|
|
const missingDir = tempDir();
|
|
const missingStore = initializePluginState({ userDataPath: missingDir });
|
|
assert.deepEqual(missingStore.snapshot(), { version: 1, plugins: {} });
|
|
assert.equal(existsSync(join(missingDir, openPetsPluginStateFileName)), true);
|
|
|
|
const corruptDir = tempDir();
|
|
const corruptPath = join(corruptDir, openPetsPluginStateFileName);
|
|
writeFileSync(corruptPath, "{not json", "utf8");
|
|
const corruptStore = initializePluginState({ userDataPath: corruptDir });
|
|
assert.deepEqual(corruptStore.listRecords(), []);
|
|
assert.deepEqual(JSON.parse(readFileSync(corruptPath, "utf8")) as unknown, { version: 1, plugins: {} });
|
|
|
|
const malformedRecordDir = tempDir();
|
|
const malformedRecordPath = join(malformedRecordDir, openPetsPluginStateFileName);
|
|
writeFileSync(
|
|
malformedRecordPath,
|
|
JSON.stringify({
|
|
version: 1,
|
|
plugins: {
|
|
invalid: { ...makeRecord({ id: "invalid" }), config: [] },
|
|
valid: makeRecord({ id: "valid" }),
|
|
},
|
|
}),
|
|
"utf8",
|
|
);
|
|
const malformedRecordStore = initializePluginState({ userDataPath: malformedRecordDir });
|
|
assert.deepEqual(malformedRecordStore.listRecords().map((record) => record.id), ["valid"]);
|
|
|
|
const statePath = join(tempDir(), "custom-plugin-state.json");
|
|
const store = initializePluginState({ statePath });
|
|
const firstRecord = makeRecord({ approvedPermissions: ["timer", "familiar:speak"] });
|
|
store.upsertRecord(firstRecord);
|
|
|
|
const reloadedStore = new PluginStateStore({ statePath });
|
|
reloadedStore.read();
|
|
assert.deepEqual(reloadedStore.getRecord("stretch-reminder"), {
|
|
...firstRecord,
|
|
approvedPermissions: ["familiar:speak", "timer"],
|
|
});
|
|
|
|
const secondRecord = makeRecord({ id: "water-reminder", version: "2.0.0", source: "local", approvedPermissions: ["familiar:reaction"] });
|
|
reloadedStore.upsertRecord(secondRecord);
|
|
assert.deepEqual(reloadedStore.listRecords().map((record) => record.id), ["stretch-reminder", "water-reminder"]);
|
|
reloadedStore.removeRecord("water-reminder");
|
|
assert.equal(reloadedStore.getRecord("water-reminder"), undefined);
|
|
|
|
assert.equal(reloadedStore.setEnabled("stretch-reminder", false).enabled, false);
|
|
assert.equal(reloadedStore.setEnabled("stretch-reminder", true).enabled, true);
|
|
|
|
assert.deepEqual(reloadedStore.updateConfig("stretch-reminder", { intervalMinutes: 10, message: "Move" }).config, {
|
|
intervalMinutes: 10,
|
|
message: "Move",
|
|
});
|
|
assert.deepEqual(reloadedStore.updateConfig("stretch-reminder", { message: "Stretch" }).config, {
|
|
intervalMinutes: 10,
|
|
message: "Stretch",
|
|
});
|
|
|
|
assert.equal(reloadedStore.setBrokenReason("stretch-reminder", "Manifest missing").brokenReason, "Manifest missing");
|
|
assert.equal(reloadedStore.clearBrokenReason("stretch-reminder").brokenReason, undefined);
|
|
|
|
assert.throws(() => reloadedStore.upsertRecord(makeRecord({ id: "bad-dupe", approvedPermissions: ["timer", "timer"] })), /Duplicate plugin permission/);
|
|
assert.throws(
|
|
() => reloadedStore.upsertRecord(makeRecord({ id: "bad-invalid", approvedPermissions: ["timer", "bad" as "timer"] })),
|
|
/Invalid plugin permission/,
|
|
);
|
|
|
|
const uninitializedStore = new PluginStateStore({ statePath: join(tempDir(), "uninitialized.json") });
|
|
assert.throws(() => uninitializedStore.snapshot(), /not been initialized/);
|
|
assert.throws(() => uninitializedStore.listRecords(), /not been initialized/);
|
|
assert.throws(() => uninitializedStore.getRecord("stretch-reminder"), /not been initialized/);
|
|
assert.throws(() => uninitializedStore.setEnabled("stretch-reminder", true), /not been initialized/);
|
|
|
|
assert.throws(() => reloadedStore.setEnabled("missing-plugin", true), /Plugin is not installed/);
|
|
assert.throws(() => reloadedStore.updateConfig("missing-plugin", { ok: true }), /Plugin is not installed/);
|
|
assert.throws(() => reloadedStore.setBrokenReason("missing-plugin", "broken"), /Plugin is not installed/);
|
|
assert.throws(() => reloadedStore.clearBrokenReason("missing-plugin"), /Plugin is not installed/);
|
|
|
|
for (const badValue of [undefined, Number.NaN, Infinity, -Infinity, BigInt(1), new Date(), new Map(), new Set(), () => true, Symbol("bad")]) {
|
|
assert.throws(() => reloadedStore.updateConfig("stretch-reminder", { badValue }), /JSON-compatible/);
|
|
}
|
|
assert.throws(() => reloadedStore.updateConfig("stretch-reminder", { nested: { badValue: Number.NaN } }), /JSON-compatible/);
|
|
assert.throws(() => reloadedStore.updateConfig("stretch-reminder", { list: ["ok", undefined] }), /JSON-compatible/);
|
|
assert.throws(() => reloadedStore.upsertRecord(makeRecord({ id: "bad-config-date", config: { when: new Date() } })), /JSON-compatible/);
|
|
assert.throws(() => reloadedStore.upsertRecord(makeRecord({ id: "bad-config-bigint", config: { count: BigInt(1) } })), /JSON-compatible/);
|
|
|
|
reloadedStore.upsertRecord(makeRecord({ id: "clone-check", config: { nested: { message: "original" }, items: ["a"] } }));
|
|
const snapshot = reloadedStore.snapshot();
|
|
(snapshot.plugins["clone-check"].config.nested as { message: string }).message = "changed";
|
|
(snapshot.plugins["clone-check"].config.items as string[]).push("b");
|
|
assert.deepEqual(reloadedStore.getRecord("clone-check")?.config, { nested: { message: "original" }, items: ["a"] });
|
|
const listedRecord = reloadedStore.listRecords().find((record) => record.id === "clone-check");
|
|
assert.ok(listedRecord);
|
|
(listedRecord.config.nested as { message: string }).message = "listed change";
|
|
assert.deepEqual(reloadedStore.getRecord("clone-check")?.config, { nested: { message: "original" }, items: ["a"] });
|
|
const fetchedRecord = reloadedStore.getRecord("clone-check");
|
|
assert.ok(fetchedRecord);
|
|
(fetchedRecord.config.nested as { message: string }).message = "fetched change";
|
|
assert.deepEqual(reloadedStore.getRecord("clone-check")?.config, { nested: { message: "original" }, items: ["a"] });
|
|
|
|
const updateStorePath = join(tempDir(), "update-plugin-state.json");
|
|
const updateStore = initializePluginState({ statePath: updateStorePath });
|
|
updateStore.upsertRecord(
|
|
makeRecord({
|
|
id: "update-check",
|
|
update: { availableVersion: "1.1.0", checkedAt: "2026-05-18T00:00:00.000Z", catalogUrl: "https://example.test/catalog.json" },
|
|
}),
|
|
);
|
|
updateStore.upsertRecord(makeRecord({ id: "update-normalized", update: { availableVersion: "", checkedAt: " ", catalogUrl: undefined } }));
|
|
const updateReload = new PluginStateStore({ statePath: updateStorePath });
|
|
updateReload.read();
|
|
assert.deepEqual(updateReload.getRecord("update-check")?.update, {
|
|
availableVersion: "1.1.0",
|
|
checkedAt: "2026-05-18T00:00:00.000Z",
|
|
catalogUrl: "https://example.test/catalog.json",
|
|
});
|
|
assert.equal(updateReload.getRecord("update-normalized")?.update, undefined);
|
|
|
|
const orderingStore = initializePluginState({ statePath: join(tempDir(), "ordering-plugin-state.json") });
|
|
orderingStore.upsertRecord(makeRecord({ id: "order-a", approvedPermissions: ["timer", "familiar:reaction", "familiar:speak"] }));
|
|
orderingStore.upsertRecord(makeRecord({ id: "order-b", approvedPermissions: ["familiar:reaction", "familiar:speak", "timer"] }));
|
|
assert.deepEqual(orderingStore.getRecord("order-a")?.approvedPermissions, ["familiar:speak", "familiar:reaction", "timer"]);
|
|
assert.deepEqual(orderingStore.getRecord("order-b")?.approvedPermissions, ["familiar:speak", "familiar:reaction", "timer"]);
|
|
|
|
console.error("Plugin state validation passed.");
|
|
|
|
function tempDir(): string {
|
|
return mkdtempSync(join(tmpdir(), "familiaros-plugin-state-"));
|
|
}
|
|
|
|
function makeRecord(patch: Partial<PluginStateRecord> = {}): PluginStateRecord {
|
|
const id = patch.id ?? "stretch-reminder";
|
|
return Object.fromEntries(Object.entries({
|
|
id,
|
|
version: patch.version ?? "1.0.0",
|
|
manifestPath: patch.manifestPath ?? `/tmp/${id}/familiaros.plugin.json`,
|
|
installPath: patch.installPath ?? `/tmp/${id}`,
|
|
source: patch.source ?? "catalog",
|
|
enabled: patch.enabled ?? true,
|
|
approvedPermissions: patch.approvedPermissions ?? ["familiar:speak", "timer"],
|
|
config: patch.config ?? {},
|
|
brokenReason: patch.brokenReason,
|
|
update: patch.update,
|
|
}).filter(([, value]) => value !== undefined)) as unknown as PluginStateRecord;
|
|
}
|