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 }); }