Lumi/scripts/verify-plugin-update-preserves-data.js
2026-07-17 22:10:00 +02:00

112 lines
4.9 KiB
JavaScript

const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const {
replacePluginDirectory,
resetDirectoryForFullUpdate
} = require("../src/services/update-manager");
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-plugin-update-test-"));
try {
const source = path.join(root, "source");
const target = path.join(root, "target");
fs.mkdirSync(path.join(source, "data", "config"), { recursive: true });
fs.mkdirSync(path.join(target, "data", "models"), { recursive: true });
fs.writeFileSync(path.join(source, "plugin.json"), '{"id":"test"}');
fs.writeFileSync(path.join(source, "index.js"), "module.exports = 'new';");
fs.writeFileSync(path.join(source, "data", "config", "default.json"), '{"default":true}');
fs.writeFileSync(path.join(target, "index.js"), "module.exports = 'old';");
fs.writeFileSync(path.join(target, "stale.js"), "stale");
const model = path.join(target, "data", "models", "large.gguf");
const descriptor = fs.openSync(model, "w");
fs.ftruncateSync(descriptor, 3 * 1024 * 1024 * 1024);
fs.closeSync(descriptor);
replacePluginDirectory(source, target, { preserveData: true });
assert.equal(fs.readFileSync(path.join(target, "index.js"), "utf8"), "module.exports = 'new';");
assert.equal(fs.existsSync(path.join(target, "stale.js")), false);
assert.equal(fs.statSync(model).size, 3 * 1024 * 1024 * 1024);
assert.equal(fs.existsSync(path.join(target, "data", "config", "default.json")), false);
const failedSource = path.join(root, "failed-source");
fs.mkdirSync(failedSource, { recursive: true });
fs.writeFileSync(path.join(failedSource, "plugin.json"), '{"id":"test"}');
fs.writeFileSync(path.join(failedSource, "index.js"), "module.exports = 'must-not-land';");
const originalRename = fs.renameSync;
fs.renameSync = (from, to) => {
if (String(from).includes(".target.update-") && to === target) {
throw Object.assign(new Error("simulated install lock"), { code: "EPERM" });
}
return originalRename(from, to);
};
try {
assert.throws(
() => replacePluginDirectory(failedSource, target, { preserveData: true }),
/simulated install lock/
);
} finally {
fs.renameSync = originalRename;
}
assert.equal(fs.readFileSync(path.join(target, "index.js"), "utf8"), "module.exports = 'new';");
assert.equal(fs.statSync(model).size, 3 * 1024 * 1024 * 1024);
assert.equal(fs.readdirSync(root).some((name) => name.includes(".target.update-") || name.includes(".target.backup-")), false);
const install = path.join(root, "install");
for (const relative of [
"data/feedback/report.json",
"data/lumi_ai/models/model.gguf",
"plugins/example/data/settings.json",
"knowledge/community/local.md",
"knowledge/corrections/fix.md",
"knowledge/core/generated.md",
"knowledge/core/local.md",
"knowledge/plugins/generated.md",
"config/local.json",
"src/stale.js"
]) {
const file = path.join(install, relative);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, relative);
}
fs.writeFileSync(
path.join(install, "knowledge", "core", "generated.md"),
"---\ngenerated: true\neditable: false\n---\ngenerated"
);
fs.writeFileSync(
path.join(install, "knowledge", "plugins", "generated.md"),
"---\ngenerated: true\neditable: false\n---\ngenerated"
);
fs.writeFileSync(
path.join(install, "knowledge", "core", "local.md"),
"---\ngenerated: false\neditable: true\n---\nlocal"
);
resetDirectoryForFullUpdate(install);
for (const relative of [
"data/feedback/report.json",
"data/lumi_ai/models/model.gguf",
"plugins/example/data/settings.json",
"knowledge/community/local.md",
"knowledge/corrections/fix.md",
"knowledge/core/local.md",
"config/local.json"
]) {
assert.equal(fs.existsSync(path.join(install, relative)), true, `${relative} should be preserved`);
}
assert.equal(fs.existsSync(path.join(install, "knowledge/core/generated.md")), false);
assert.equal(fs.existsSync(path.join(install, "knowledge/plugins/generated.md")), false);
assert.equal(fs.existsSync(path.join(install, "src/stale.js")), false);
const managerSource = fs.readFileSync(path.join(__dirname, "..", "src", "services", "update-manager.js"), "utf8");
const updateDocs = fs.readFileSync(path.join(__dirname, "..", "docs", "updates.md"), "utf8");
assert(managerSource.includes("Automatic rollback also failed"));
assert.equal((managerSource.match(/restoreSnapshot\(record\.id/g) || []).length >= 2, true);
assert(updateDocs.includes("Preserved Local Data"));
assert(updateDocs.includes("knowledge/community/"));
assert(updateDocs.includes("AI models/runtimes"));
console.log("Update preservation verification passed: core paths, plugin data, transactional replacement, and failed-update rollback wiring.");
} finally {
fs.rmSync(root, { recursive: true, force: true });
}