Files
coa-server-manager/tools/server-update-refresh.test.mjs
T

107 lines
5.9 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { registerHooks } from "node:module";
let applyFailure = false;
let stateFailure = false;
let pending = null;
let serverRunning = true;
const calls = [];
globalThis.__hotfixApi = {
status: async () => ({ observed: Object.fromEntries(["mysql", "auth", "world"].map(key => [key, { state: serverRunning ? "running" : "stopped" }])) }),
stop: async () => { calls.push("stop"); },
applyUpdate: async () => { calls.push("apply"); if (applyFailure) throw new Error("SQL failure"); return { txn: { state: "committed", to_version: "2" } }; },
pendingUpdate: async () => { calls.push("disk"); if (stateFailure) throw new Error("damaged journal"); return pending; },
checkUpdate: async () => ({ from_version: "2", to_version: "2", items: [], conflicts: [], migrations: 1, pending_migrations: 0 }),
};
registerHooks({ resolve(specifier, context, next) {
if (specifier === "react") return { shortCircuit: true, url: "data:text/javascript,export const useSyncExternalStore=(subscribe,getSnapshot)=>getSnapshot();export const useState=()=>{throw new Error('React state is not used in this test');};" };
if (specifier === "@/lib/serverLifecycle") return { shortCircuit: true, url: new URL("../src/lib/serverLifecycle.ts", import.meta.url).href };
if (specifier === "@/lib/api") return { shortCircuit: true, url: "data:text/javascript,export const api=globalThis.__hotfixApi;export const asUiError=e=>({human:{code:'unknown'},technical:e.message});" };
if (specifier === "@tauri-apps/api/event") return { shortCircuit: true, url: "data:text/javascript,export const listen=async()=>()=>{};" };
if (specifier === "./serverUpdateStatus" && context.parentURL?.endsWith("/serverUpdate.ts")) return { shortCircuit: true, url: new URL("../src/lib/serverUpdateStatus.ts", import.meta.url).href };
if (specifier === "./updatePrefs" && context.parentURL?.endsWith("/serverUpdate.ts")) return { shortCircuit: true, url: new URL("../src/lib/updatePrefs.ts", import.meta.url).href };
return next(specifier, context);
} });
const { applyServerUpdate, refreshServerUpdateState, checkServerUpdate, useServerUpdate } = await import("../src/lib/serverUpdate.ts");
test("a failed update rereads its unfinished transaction from disk", async () => {
calls.length = 0;
applyFailure = true;
pending = { id: "interrupted", state: "failed", message: "Recovery required" };
assert.equal(await applyServerUpdate("failed-case", { choices: {} }), false);
assert.deepEqual(calls, ["apply", "disk"]);
assert.equal(await refreshServerUpdateState("failed-case"), pending);
});
test("unreadable state blocks another apply until a successful reread", async () => {
calls.length = 0;
applyFailure = false;
stateFailure = true;
await applyServerUpdate("corrupt-case", { choices: {} });
assert.equal(await applyServerUpdate("corrupt-case", { choices: {} }), false);
assert.deepEqual(calls, ["apply", "disk"]);
stateFailure = false;
pending = null;
assert.equal(await refreshServerUpdateState("corrupt-case"), null);
assert.equal(await applyServerUpdate("corrupt-case", { choices: {} }), true);
assert.deepEqual(calls, ["apply", "disk", "disk", "apply", "disk"]);
});
test("refresh after recovery returns the latest disk state, then its absence", async () => {
pending = { id: "recovery", state: "failed", message: "Restore failed at database verification" };
assert.equal(await refreshServerUpdateState("recovery-case"), pending);
pending = null;
assert.equal(await refreshServerUpdateState("recovery-case"), null);
});
test("successful update keeps the existing-character warning across disk refresh", async () => {
const original = globalThis.__hotfixApi.applyUpdate;
try {
applyFailure = false;
stateFailure = false;
pending = null;
globalThis.__hotfixApi.applyUpdate = async () => ({ txn: { state: "committed", to_version: "2" }, preexisting_character_count: 174 });
assert.equal(await applyServerUpdate("old-character-warnings", { choices: {} }), true);
await refreshServerUpdateState("old-character-warnings");
assert.deepEqual(useServerUpdate("old-character-warnings").result, { committed: "2", preexistingCharacters: 174 });
} finally { globalThis.__hotfixApi.applyUpdate = original; }
});
test("a stopped server is left stopped after a successful update by default", async () => {
calls.length = 0;
serverRunning = false;
applyFailure = false;
stateFailure = false;
pending = null;
try {
assert.equal(await applyServerUpdate("stopped-case", { choices: {} }), true);
assert.deepEqual(calls, ["apply", "stop", "disk"]);
} finally { serverRunning = true; }
});
test("a rejected recheck removes an earlier cached update offer", async () => {
const original = globalThis.__hotfixApi.checkUpdate;
try {
globalThis.__hotfixApi.checkUpdate = async () => ({ from_version: "0.261005.22", to_version: "0.261005.24", items: [], migrations: 1, pending_migrations: 1 });
await checkServerUpdate("stale-offer");
assert.equal(useServerUpdate("stale-offer").available, true);
globalThis.__hotfixApi.checkUpdate = async () => { throw new Error("Installed version is newer than package"); };
await checkServerUpdate("stale-offer");
assert.equal(useServerUpdate("stale-offer").available, false);
assert.equal(useServerUpdate("stale-offer").preview, null);
} finally { globalThis.__hotfixApi.checkUpdate = original; }
});
test("a successful recheck keeps the last failed update details visible", async () => {
applyFailure = true;
stateFailure = false;
pending = null;
try {
assert.equal(await applyServerUpdate("visible-error", { choices: {} }), false);
await checkServerUpdate("visible-error");
const state = useServerUpdate("visible-error");
assert.equal(state.applying, false);
assert.equal(state.available, false);
assert.equal(state.uiError.technical, "SQL failure");
} finally { applyFailure = false; }
});