open-standards/wai/quantum-studio/test/update.test.mjs
Transaction Science 97935030d3 wai-quantum-desktop: signed updates, offered in the Studio
A copy installed by its own installer (the macOS app, the AppImage, the
Debian or RPM package, the Windows installer) asks latest.json on the
pre-release wai-quantum-desktop-latest for a newer version as it opens.
The Studio offers it in a bar above the frame (src/update.js, its words
in English and Chinese); nothing installs until the person says so, and
then only an update whose signature verifies with the public key in
tauri.conf.json and names the version announced (requireSignedVersion),
after which the app restarts into it.

- The updater plugin 2.14.0 registered; commands update_check and
  update_install. A copy built from source or unpacked by a package
  manager does not update itself, and the check answers why
  (installed_by / updates_itself).
- verify-update FILE SIG VERSION: the same signature and version check
  from the command line, for the release to run on every update it signs.
- update-status: the installer this copy came from, as JSON, for the
  release to ask of each installer's copy.
- tauri.conf.json: the updater's one https endpoint, an empty public key
  until the owner makes the release key, update artifacts off unless a
  release turns them on, the macOS app signed ad hoc unless a Developer ID
  is given, and the publisher and homepage the packages carry.
- icons/icon.ico and icons/icon.icns, made from icons/icon.png by the
  bundler's icon command: a Windows build needs the .ico for its resource
  file (the app had not been built on Windows before), and the macOS app
  takes the .icns.
- Tests: the published test vectors held to the key and the signed
  version (tampered bytes, another key, another version, no version, no
  key, bad base64 all refused); which copies update themselves; the
  configuration's endpoint and flags; test/update.test.mjs for the bar.
- New crates in the lockfile at versions at least two weeks old, except
  webpki-root-certs at 1.0.10, the first release with RUSTSEC-2026-0383
  fixed.
- The Studio's README and its translation say what the bar does.
- The views page rebuilt (bash wai/wai-quantum-views/build.sh), since it
  is built from the Studio's English words and styles.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 10:23:52 -04:00

66 lines
3.2 KiB
JavaScript

// The desktop app's update offer, without a browser: a newer version is told
// of in words naming both versions, nothing else is; nothing installs until
// asked, and only the update told of; a refusal (a signature that does not
// verify, a download that fails) is reported, and a check that cannot reach
// the manifest stays quiet.
//
// node test/update.test.mjs
import { Updates, updateNotice } from '../src/update.js';
let fails = 0;
const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
// The app's commands, faked: `check` is what update_check answers, `install` what update_install does.
function fake(answer, install = () => JSON.stringify({ error: 'the update was not installed: the signature does not verify' })) {
const calls = [];
const invoke = async (cmd, args) => {
calls.push([cmd, args]);
if (cmd === 'update_check') { if (answer instanceof Error) throw answer; return JSON.stringify(answer); }
if (cmd === 'update_install') return install();
return JSON.stringify({ error: `no command ${cmd}` });
};
return { calls, invoke };
}
const available = { state: 'available', version: '0.2.0', current: '0.1.0', notes: 'n' };
{
const words = updateNotice(available);
check(typeof words === 'string' && words.includes('0.2.0') && words.includes('0.1.0') && /signature/.test(words), `a newer version is told of, with both versions and the signature check (${words})`);
for (const a of [{ state: 'current', version: '0.1.0' }, { state: 'off', reason: 'this build carries no update key' }, { error: 'offline' }, null, undefined]) {
check(updateNotice(a) === null, `nothing is said for ${JSON.stringify(a)}`);
}
}
{
const f = fake(available);
const u = new Updates(f.invoke);
const v = await u.check();
check(v.state === 'available' && f.calls.length === 1 && f.calls[0][0] === 'update_check', 'a check asks the app once and installs nothing');
let err = '';
try { await u.install(); } catch (e) { err = e.message; }
check(f.calls[1]?.[0] === 'update_install' && /signature does not verify/.test(err), `an install asked for runs the app's install, and its refusal is reported (${err})`);
}
{
const f = fake({ state: 'off', reason: 'this copy was built from source' });
const u = new Updates(f.invoke);
await u.check();
let err = '';
try { await u.install(); } catch (e) { err = e.message; }
check(err === 'No update is waiting.' && !f.calls.some((c) => c[0] === 'update_install'), 'with no update told of, an install asks the app nothing');
}
{
const f = fake(available, () => JSON.stringify({ restarting: true }));
const u = new Updates(f.invoke);
await u.check();
let threw = false;
try { await u.install(); } catch (e) { threw = true; }
check(!threw && f.calls.filter((c) => c[0] === 'update_install').length === 1, 'an update that installs is installed once');
}
{
const f = fake(new Error('the app has no such command'));
const u = new Updates(f.invoke);
const v = await u.check();
check(v.error === 'the app has no such command' && updateNotice(v) === null && u.waiting === null, 'a check that fails is an answer that says nothing to the reader');
}
console.log(fails ? `${fails} check(s) failed` : 'every check passed');
process.exit(fails ? 1 : 0);