English spellcheck suggestions with Add-to-dictionary in the native context menu plus a Settings toggle. Typewriter caret-centering mode. OS file associations for .md/.txt and Open File wording. Updater: release URL surfaced, single progress listener, two-byte tarball check, installer wait guard. Search/installer test suites registered; chapter-list layout polish; ignore KDE .directory files.
123 lines
5.1 KiB
JavaScript
123 lines
5.1 KiB
JavaScript
// Sandbox execution test for the updater handoff script (buildInstallerScript).
|
|
// Runs the generated /bin/sh script against fake install dirs in os.tmpdir() —
|
|
// never the real ~/.local/share/Folio — proving the swap works and failures
|
|
// always relaunch the OLD version.
|
|
// Run with: node tests/run-update-installer-test.mjs
|
|
import { execFileSync, spawn } from "child_process";
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import url from "url";
|
|
import { bundleForTest } from "./helpers/bundle.mjs";
|
|
|
|
const { mod: u, cleanupBundle } = await bundleForTest({
|
|
name: "folio-update-script",
|
|
entryPoints: ["src/main/update.ts"],
|
|
});
|
|
|
|
let fails = 0;
|
|
function ok(cond, msg) {
|
|
if (cond) console.log(" ok -", msg);
|
|
else { fails++; console.error("FAIL -", msg); }
|
|
}
|
|
|
|
function sleepPid(ms) {
|
|
// Short-lived stand-in for the app process the script waits on.
|
|
const p = spawn("sleep", [String(ms / 1000)], { stdio: "ignore" });
|
|
return p.pid;
|
|
}
|
|
|
|
// A pid that is dead AND reaped (kill -0 fails), so the script proceeds
|
|
// without waiting. NOTE: a Node-spawned child that just exited stays a
|
|
// zombie while our event loop is blocked inside execFileSync — and kill -0
|
|
// succeeds on zombies — so the pid must be observed dead before running the
|
|
// script, not merely spawned with a short sleep.
|
|
async function deadPid() {
|
|
const p = spawn("sleep", ["0.01"], { stdio: "ignore" });
|
|
const pid = p.pid;
|
|
p.on("error", () => {});
|
|
for (let i = 0; i < 200; i++) {
|
|
try {
|
|
process.kill(pid, 0);
|
|
} catch {
|
|
return pid;
|
|
}
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
}
|
|
throw new Error("helper process would not die");
|
|
}
|
|
|
|
function runScript(script, timeoutMs = 30000) {
|
|
execFileSync("/bin/sh", [script], { timeout: timeoutMs, stdio: "ignore" });
|
|
}
|
|
|
|
// Fake "installed" app tree: an executable `folio` stub + a data file.
|
|
function makeInstall(box, versionMarker) {
|
|
const dir = path.join(box, "install");
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, "folio"), `#!/bin/sh\necho ${versionMarker}\n`, { mode: 0o755 });
|
|
fs.writeFileSync(path.join(dir, "other.txt"), "keepme");
|
|
return dir;
|
|
}
|
|
|
|
// Fake release tarball with an electron-builder-style nested top-level dir.
|
|
function makeTarball(box, versionMarker) {
|
|
const staged = path.join(box, "staged", "Folio-linux-x64");
|
|
fs.mkdirSync(staged, { recursive: true });
|
|
fs.writeFileSync(path.join(staged, "folio"), `#!/bin/sh\necho ${versionMarker}\n`, { mode: 0o755 });
|
|
fs.writeFileSync(path.join(staged, "extra.txt"), "newfile");
|
|
const tarball = path.join(box, "release.tar.gz");
|
|
execFileSync("tar", ["-czf", tarball, "-C", path.join(box, "staged"), "Folio-linux-x64"]);
|
|
return tarball;
|
|
}
|
|
|
|
// Relaunch stub records that it was invoked.
|
|
function makeRelaunch(box) {
|
|
const marker = path.join(box, "relaunched");
|
|
const launcher = path.join(box, "relaunch.sh");
|
|
fs.writeFileSync(launcher, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\n`, { mode: 0o755 });
|
|
return { launcher, marker };
|
|
}
|
|
|
|
function writeScript(box, spec) {
|
|
const script = path.join(box, "installer.sh");
|
|
fs.writeFileSync(script, u.buildInstallerScript(spec), { mode: 0o700 });
|
|
return script;
|
|
}
|
|
|
|
// --- success path: valid tarball replaces the install, relaunches ---
|
|
{
|
|
const box = fs.mkdtempSync(path.join(os.tmpdir(), "folio-upd-ok-"));
|
|
const install = makeInstall(box, "OLD");
|
|
const tarball = makeTarball(box, "NEW");
|
|
const { launcher, marker } = makeRelaunch(box);
|
|
const pid = await deadPid();
|
|
runScript(writeScript(box, { pid, tarball, installDir: install, relaunch: launcher }));
|
|
await new Promise((r) => setTimeout(r, 800)); // relaunched in background (&)
|
|
ok(fs.readFileSync(path.join(install, "folio"), "utf-8").includes("NEW"), "install swapped to new version");
|
|
ok(fs.readFileSync(path.join(install, "extra.txt"), "utf-8") === "newfile", "new files land in install");
|
|
ok(fs.existsSync(marker), "relaunch invoked after swap");
|
|
ok(!fs.existsSync(tarball), "tarball cleaned up after success");
|
|
ok(!fs.readdirSync(path.dirname(install)).some((e) => e.includes(".old.")), "old tree pruned after success");
|
|
fs.rmSync(box, { recursive: true, force: true });
|
|
}
|
|
|
|
// --- failure path: corrupt tarball keeps the old install, still relaunches ---
|
|
{
|
|
const box = fs.mkdtempSync(path.join(os.tmpdir(), "folio-upd-bad-"));
|
|
const install = makeInstall(box, "OLD");
|
|
const tarball = path.join(box, "release.tar.gz");
|
|
fs.writeFileSync(tarball, "this is not a gzip stream");
|
|
const { launcher, marker } = makeRelaunch(box);
|
|
const pid = await deadPid();
|
|
runScript(writeScript(box, { pid, tarball, installDir: install, relaunch: launcher }));
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
ok(fs.readFileSync(path.join(install, "folio"), "utf-8").includes("OLD"), "old install intact after bad tarball");
|
|
ok(fs.existsSync(marker), "relaunch still invoked after failure");
|
|
ok(!fs.readdirSync(path.dirname(install)).some((e) => e.includes(".old.")), "no .old residue after failure");
|
|
fs.rmSync(box, { recursive: true, force: true });
|
|
}
|
|
|
|
cleanupBundle();
|
|
if (fails) { console.error(`${fails} installer check(s) FAILED`); process.exit(1); }
|
|
console.log("All installer checks passed.");
|