Files
NetMesh/electron/plugins/packageStore.test.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

373 lines
16 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const { createHash } = require("node:crypto");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const test = require("node:test");
const { PluginDatabase } = require("./database.cjs");
const {
ARCHIVE_SNAPSHOT_FILE,
PackageStore,
REMOVAL_METADATA_FILE,
REMOVED_PLUGIN_DIRECTORY,
} = require("./packageStore.cjs");
const { createPluginPaths } = require("./paths.cjs");
async function createPackage(root, overrides = {}) {
const source = path.join(root, `source-${Math.random().toString(16).slice(2)}`);
await fs.promises.mkdir(path.join(source, "dist"), { recursive: true });
const pluginManifest = {
manifestVersion: 1,
id: "com.example.runtime-test",
name: "runtime-test",
version: "1.0.0",
publisher: "example",
engines: { netcatty: ">=0.0.0", api: ">=0.1.0-internal <0.2.0" },
main: { browser: "dist/index.js" },
...overrides.manifest,
};
await Promise.all([
fs.promises.writeFile(
path.join(source, "netcatty.plugin.json"),
`${JSON.stringify(pluginManifest, null, 2)}\n`,
),
fs.promises.writeFile(path.join(source, "dist/index.js"), overrides.source ?? "export default {};\n"),
]);
const archive = path.join(root, `${pluginManifest.id}-${Date.now()}-${Math.random()}.ncpkg`);
const { buildPluginPackage } = await import("@netcatty/plugin-cli");
await buildPluginPackage(source, archive);
return { archive, manifest: pluginManifest };
}
function createStore(context, options = {}) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-plugin-store-"));
context.after(() => fs.rmSync(root, { recursive: true, force: true }));
const paths = createPluginPaths(root);
const database = new PluginDatabase(paths.database);
context.after(() => {
try { database.close(); } catch {}
});
const store = new PackageStore({
paths,
database,
netcattyVersion: "0.0.0",
apiVersion: "0.1.0-internal",
supportedFeatures: [],
...options,
});
return { root, paths, database, store };
}
test("package install stages, validates, atomically publishes, and records the active version", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
assert.equal(installed.id, pluginPackage.manifest.id);
assert.equal(installed.enabled, true);
const packageRoot = fixture.store.resolvePackageRoot(installed);
assert.equal(
await fs.promises.readFile(path.join(packageRoot, "dist/index.js"), "utf8"),
"export default {};\n",
);
const versionDirectory = path.dirname(packageRoot);
const retainedArchive = path.join(versionDirectory, ARCHIVE_SNAPSHOT_FILE);
assert.equal((await fs.promises.stat(retainedArchive)).isFile(), true);
assert.equal(
createHash("sha256").update(await fs.promises.readFile(retainedArchive)).digest("hex"),
installed.archiveSha256,
);
const installMetadata = JSON.parse(await fs.promises.readFile(
path.join(versionDirectory, "install.json"),
"utf8",
));
assert.match(installMetadata.contentSha256, /^[a-f0-9]{64}$/u);
assert.deepEqual(await fs.promises.readdir(fixture.paths.staging), []);
});
test("runtime preparation rejects changed files and disables the active plugin", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
const packageRoot = fixture.store.resolvePackageRoot(installed);
await fs.promises.writeFile(path.join(packageRoot, "dist/index.js"), "export default { tampered: true };\n");
await assert.rejects(
fixture.store.preparePackageRoot(installed),
/do not match the retained package archive/,
);
const failed = fixture.database.getActivePlugin(installed.id);
assert.equal(failed.enabled, false);
assert.equal(failed.runtime.status, "error");
assert.match(failed.runtime.lastError, /integrity check failed/);
});
test("runtime preparation rejects unpackaged root entries hidden from source builds", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
const packageRoot = fixture.store.resolvePackageRoot(installed);
await fs.promises.mkdir(path.join(packageRoot, "node_modules", "injected"), { recursive: true });
await fs.promises.writeFile(path.join(packageRoot, "node_modules", "injected", "index.js"), "throw 1;\n");
await assert.rejects(
fixture.store.preparePackageRoot(installed),
/unpackaged root entry: node_modules/,
);
assert.equal(fixture.database.getActivePlugin(installed.id).enabled, false);
});
test("startup keeps a corrupted committed snapshot for diagnosis and fails closed", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
const versionDirectory = path.dirname(fixture.store.resolvePackageRoot(installed));
const retainedArchive = path.join(versionDirectory, ARCHIVE_SNAPSHOT_FILE);
await fs.promises.writeFile(retainedArchive, "corrupt archive");
await fixture.store.recover();
assert.equal((await fs.promises.stat(retainedArchive)).isFile(), true);
const failed = fixture.database.getActivePlugin(installed.id);
assert.equal(failed.enabled, false);
assert.equal(failed.runtime.status, "error");
assert.match(failed.runtime.lastError, /integrity check failed/);
});
test("reinstalling identical bytes repairs a corrupted retained snapshot", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
const versionDirectory = path.dirname(fixture.store.resolvePackageRoot(installed));
const retainedArchive = path.join(versionDirectory, ARCHIVE_SNAPSHOT_FILE);
await fs.promises.writeFile(retainedArchive, "corrupt archive");
const repaired = await fixture.store.install(pluginPackage.archive);
assert.equal(repaired.enabled, true);
assert.equal(
createHash("sha256").update(await fs.promises.readFile(retainedArchive)).digest("hex"),
repaired.archiveSha256,
);
assert.equal(await fixture.store.preparePackageRoot(repaired), path.join(versionDirectory, "package"));
});
test("package install is idempotent for identical bytes and rejects version substitution", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const first = await createPackage(fixture.root);
await fixture.store.install(first.archive);
const secondVersion = await createPackage(fixture.root, {
manifest: { version: "2.0.0" },
source: "export default { version: 2 };\n",
});
await fixture.store.install(secondVersion.archive);
assert.equal(fixture.database.getActivePlugin(first.manifest.id).activeVersion, "2.0.0");
const reactivated = await fixture.store.install(first.archive, { enable: true });
assert.equal(reactivated.activeVersion, "1.0.0");
assert.equal(reactivated.enabled, true);
const changed = await createPackage(fixture.root, { source: "export default { changed: true };\n" });
await assert.rejects(
fixture.store.install(changed.archive),
/already installed with different contents/,
);
assert.deepEqual(await fs.promises.readdir(fixture.paths.staging), []);
});
test("package activation hook runs before the active version changes", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const first = await createPackage(fixture.root);
const second = await createPackage(fixture.root, {
manifest: { version: "2.0.0" },
source: "export default { version: 2 };\n",
});
await fixture.store.install(first.archive, { enable: true });
const observations = [];
const installed = await fixture.store.install(second.archive, {
beforeActivate(details) {
observations.push({
details,
activeVersion: fixture.database.getActivePlugin(first.manifest.id).activeVersion,
});
},
});
assert.equal(installed.activeVersion, "2.0.0");
assert.equal(installed.enabled, true);
assert.equal(observations.length, 1);
assert.equal(observations[0].activeVersion, "1.0.0");
assert.equal(observations[0].details.pluginId, first.manifest.id);
assert.equal(observations[0].details.version, "2.0.0");
assert.equal(observations[0].details.previousPlugin.activeVersion, "1.0.0");
assert.equal(observations[0].details.reason, "switch-active-version");
});
test("failed activation preparation preserves the previous database selection", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const first = await createPackage(fixture.root);
const second = await createPackage(fixture.root, { manifest: { version: "2.0.0" } });
await fixture.store.install(first.archive, { enable: true });
await assert.rejects(
fixture.store.install(second.archive, {
beforeActivate() { throw new Error("runtime did not stop"); },
}),
/runtime did not stop/,
);
const active = fixture.database.getActivePlugin(first.manifest.id);
assert.equal(active.activeVersion, "1.0.0");
assert.equal(active.enabled, true);
assert.equal(fixture.database.getVersion(first.manifest.id, "2.0.0"), null);
await assert.rejects(
fs.promises.stat(path.join(fixture.paths.packages, first.manifest.id, "2.0.0")),
{ code: "ENOENT" },
);
});
test("startup recovery removes staging debris and reconstructs a committed orphan disabled", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
await fs.promises.mkdir(path.join(fixture.paths.staging, "partial"));
fixture.database.removePlugin(installed.id);
await fixture.store.recover();
assert.deepEqual(await fs.promises.readdir(fixture.paths.staging), []);
const recovered = fixture.database.getActivePlugin(installed.id);
assert.equal(recovered.activeVersion, "1.0.0");
assert.equal(recovered.enabled, false);
});
test("startup recovery force-disables a newer orphan when an older version was enabled", async (context) => {
const fixture = createStore(context);
const donor = createStore(context);
await Promise.all([fixture.store.initialize(), donor.store.initialize()]);
const first = await createPackage(fixture.root);
const second = await createPackage(donor.root, { manifest: { version: "2.0.0" } });
await fixture.store.install(first.archive, { enable: true });
await donor.store.install(second.archive);
const orphanSource = path.join(donor.paths.packages, first.manifest.id, "2.0.0");
const orphanTarget = path.join(fixture.paths.packages, first.manifest.id, "2.0.0");
await fs.promises.cp(orphanSource, orphanTarget, { recursive: true, errorOnExist: true });
assert.equal(fixture.database.getActivePlugin(first.manifest.id).enabled, true);
await fixture.store.recover();
const recovered = fixture.database.getActivePlugin(first.manifest.id);
assert.equal(recovered.activeVersion, "2.0.0");
assert.equal(recovered.enabled, false);
});
test("startup recovery discards incomplete removal staging before a package was moved", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const emptyRemoval = path.join(fixture.paths.staging, "remove-empty");
const partialRemoval = path.join(fixture.paths.staging, "remove-partial");
await fs.promises.mkdir(emptyRemoval);
await fs.promises.mkdir(partialRemoval);
await fs.promises.writeFile(path.join(partialRemoval, REMOVAL_METADATA_FILE), "{\"pluginId\":");
await fixture.store.recover();
assert.deepEqual(await fs.promises.readdir(fixture.paths.staging), []);
});
test("startup recovery keeps an unidentified moved package fail-closed", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
const removalDirectory = path.join(fixture.paths.staging, "remove-corrupt-metadata");
const removedPluginPath = path.join(removalDirectory, REMOVED_PLUGIN_DIRECTORY);
await fs.promises.mkdir(removalDirectory);
await fs.promises.rename(path.join(fixture.paths.packages, installed.id), removedPluginPath);
await fs.promises.writeFile(path.join(removalDirectory, REMOVAL_METADATA_FILE), "{\"pluginId\":");
await assert.rejects(fixture.store.recover());
assert.equal((await fs.promises.stat(removedPluginPath)).isDirectory(), true);
assert.equal(fixture.database.getActivePlugin(installed.id).enabled, true);
});
test("startup recovery completes or rolls back an interrupted uninstall", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
const installedPluginPath = path.join(fixture.paths.packages, installed.id);
const stageRemoval = async (name) => {
const removalDirectory = path.join(fixture.paths.staging, name);
await fs.promises.mkdir(removalDirectory, { mode: 0o700 });
await fs.promises.writeFile(
path.join(removalDirectory, REMOVAL_METADATA_FILE),
`${JSON.stringify({ pluginId: installed.id })}\n`,
);
await fs.promises.rename(
installedPluginPath,
path.join(removalDirectory, REMOVED_PLUGIN_DIRECTORY),
);
return removalDirectory;
};
const rollbackDirectory = await stageRemoval("remove-before-database");
await fixture.store.recover();
assert.equal((await fs.promises.stat(installedPluginPath)).isDirectory(), true);
await assert.rejects(fs.promises.stat(rollbackDirectory), { code: "ENOENT" });
assert.equal(fixture.database.getActivePlugin(installed.id).activeVersion, "1.0.0");
const completionDirectory = await stageRemoval("remove-after-database");
fixture.database.removePlugin(installed.id);
await fixture.store.recover();
await assert.rejects(fs.promises.stat(completionDirectory), { code: "ENOENT" });
await assert.rejects(fs.promises.stat(installedPluginPath), { code: "ENOENT" });
assert.equal(fixture.database.getActivePlugin(installed.id), null);
});
test("uninstall syncs the package-store source before deleting its database row", async (context) => {
const synced = [];
const fixture = createStore(context, {
async syncDirectory(directory) { synced.push(directory); },
});
await fixture.store.initialize();
const pluginPackage = await createPackage(fixture.root);
const installed = await fixture.store.install(pluginPackage.archive, { enable: true });
synced.length = 0;
const removePlugin = fixture.database.removePlugin.bind(fixture.database);
fixture.database.removePlugin = (pluginId) => {
assert.equal(pluginId, installed.id);
assert.equal(synced.at(-1), fixture.paths.packages);
removePlugin(pluginId);
};
assert.equal(await fixture.store.uninstall(installed.id), true);
assert.equal(synced.includes(fixture.paths.packages), true);
});
test("failed package validation removes every extracted staging file", async (context) => {
const fixture = createStore(context);
await fixture.store.initialize();
const invalid = path.join(fixture.root, "invalid.ncpkg");
await fs.promises.writeFile(invalid, "not a zip");
await assert.rejects(fixture.store.install(invalid));
assert.deepEqual(await fs.promises.readdir(fixture.paths.staging), []);
assert.deepEqual(fixture.database.listPlugins(), []);
});