Ctrl K
ComunidadGuías…

scripts/smoke-linux-update.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// End-to-end proof that an AppImage update from the real release feed lands on
// the path the user launches, using the shipped bundle. Runs under Electron
// (electron-updater needs its net stack):
//   xvfb-run -a pnpm exec electron scripts/smoke-linux-update.mjs
import { createHash } from "node:crypto";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readdirSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const require = createRequire(import.meta.url);
const { app } = require("electron");

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const releaseDir = path.join(root, "release");

// Older than any release, so the published feed is always an upgrade.
const PRETEND_VERSION = "0.0.1";

function fail(message) {
  console.error(`[smoke-linux-update] ${message}`);
  app.exit(1);
}

// Missing is an answer, not a crash: when the install renames instead of
// overwriting, the launched path is gone and that is the finding to report.
function sha512(file) {
  try {
    return createHash("sha512").update(readFileSync(file)).digest("base64");
  } catch {
    return null;
  }
}

function onlyAppImage() {
  const matches = readdirSync(releaseDir).filter((name) => name.endsWith(".AppImage"));
  if (matches.length !== 1) throw new Error(`expected one packaged AppImage, found ${matches.length}`);
  return path.join(releaseDir, matches[0]);
}

async function main() {
  const packaged = onlyAppImage();
  const workspace = mkdtempSync(path.join(tmpdir(), "omb-update-smoke-"));
  const installDir = path.join(workspace, "opt");
  const applications = path.join(workspace, "applications");
  mkdirSync(installDir);
  mkdirSync(applications);

  // The bug shape: a version in the filename, and a launcher pinned to it.
  const launched = path.join(installDir, "OpenMausBot-0.0.1-x86_64.AppImage");
  copyFileSync(packaged, launched);
  const desktopEntry = path.join(applications, "com.openmausbot.app.desktop");
  writeFileSync(
    desktopEntry,
    `[Desktop Entry]\nName=OpenMausBot\nExec=${launched} %U\nType=Application\n`,
  );

  // Isolate every path the updater writes to.
  process.env.XDG_CACHE_HOME = path.join(workspace, "cache");
  process.env.APPIMAGE = launched;
  app.setPath("userData", path.join(workspace, "userdata"));

  // The updater that ships: Electron resolves paths inside an asar natively.
  // An explicit path runs this against an unpatched bundle, which must fail.
  const bundle =
    process.argv.find((argument) => argument.startsWith("--updater="))?.slice("--updater=".length) ??
    path.join(root, "release/linux-unpacked/resources/app.asar/electron/vendor/electron-updater.cjs");
  console.log(`[smoke-linux-update] updater under test: ${bundle}`);
  const { AppImageUpdater } = require(bundle);

  const updateConfig = path.join(workspace, "app-update.yml");
  copyFileSync(path.join(root, "release/linux-unpacked/resources/app-update.yml"), updateConfig);

  // The constructor parses the current version once, so this has to be in
  // place before the updater exists. Without it the comparison runs against
  // Electron's own version and every real release looks like a downgrade.
  app.getVersion = () => PRETEND_VERSION;

  const updater = new AppImageUpdater();
  updater.updateConfigPath = updateConfig;
  updater.forceDevUpdateConfig = true;
  updater.autoDownload = false;
  updater.logger = { info: log, warn: log, error: log, debug: () => {} };

  function log(...values) {
    console.log("   ", ...values.map(String));
  }

  console.log("[smoke-linux-update] checking the real release feed…");
  const result = await updater.checkForUpdates();
  if (!result?.updateInfo?.version) throw new Error("the feed returned no update");
  // A feed response alone is not an offer: a version comparison that decided
  // "not available" still carries updateInfo, and reading only that would let
  // this smoke pass without ever installing anything.
  if (result.isUpdateAvailable !== true) {
    throw new Error(
      `the feed's ${result.updateInfo.version} was not treated as an update over ${PRETEND_VERSION}`,
    );
  }
  const offered = result.updateInfo.version;
  console.log(`[smoke-linux-update] feed offers ${offered}`);

  const expected = result.updateInfo.files?.find((file) => file.url.endsWith(".AppImage"));
  if (!expected?.sha512) throw new Error("the feed declares no AppImage checksum");

  console.log("[smoke-linux-update] downloading…");
  await updater.downloadUpdate(result.cancellationToken);

  // Record the queued relaunch instead of starting a second app. The native
  // relauncher would otherwise run it when this fixture exits.
  let relaunched = null;
  app.relaunch = (options) => {
    relaunched = options.execPath;
    return true;
  };
  updater.spawnLog = () => { throw new Error("AppImage relaunch bypassed Electron's exit ordering"); };
  updater.quitAndInstall(true, true);

  const survivors = readdirSync(installDir);
  const desktop = readFileSync(desktopEntry, "utf8");
  const execTarget = desktop.match(/^Exec=(.*?) %U$/m)?.[1];
  console.log(`[smoke-linux-update] files left in the install directory: ${survivors.join(", ")}`);

  const checks = [
    ["the update stayed on the launched path", survivors.includes(path.basename(launched))],
    ["no second AppImage appeared", survivors.length === 1],
    [
      "the launcher still points at a real file",
      execTarget === launched && existsSync(launched),
    ],
    ["the file now holds the published build", sha512(launched) === expected.sha512],
    ["the relaunch uses the launched path", relaunched === launched],
  ];

  let failed = 0;
  for (const [label, ok] of checks) {
    console.log(`  ${ok ? "✔" : "✖"} ${label}`);
    if (!ok) failed += 1;
  }
  if (failed > 0) {
    console.error(`[smoke-linux-update] ${failed} check(s) failed; workspace kept at ${workspace}`);
    app.exit(1);
    return;
  }

  rmSync(workspace, { recursive: true, force: true });
  console.log(`[smoke-linux-update] OK — updated 0.0.1 → ${offered} in place`);
  app.exit(0);
}

app.whenReady().then(main).catch((error) => fail(error.stack ?? String(error)));