Files
OmniRoute/tests/unit/check-install-upgrade-convergence.test.ts
diegosouzapw 21c488f210 chore(release): bring main's post-tag fixes into release/v3.8.51 — sync-back step 2/2
The eighteen commits main carries beyond the cycle branch, and what each one
became here:

  already in release/v3.8.51 by its own PR (no-op, verified by content):
    b090b601a5 / 026e1cadaa  deps: nanoid 3.3.18 equal, dompurify 3.4.14 newer
    918fba5e39  .gitignore: /_tasks already anchored
    5f0a394091  #10026 hide health-check-excluded models — same helper, 5 call sites
    c68cda7dfb  #11075 shared passthrough providers — superseded by #11071/#11078
  superseded, one piece kept:
    ca23eed77c  #10055 memoize models.dev pricing — the cycle memoizes on the
                catalog cache version already; only the resetDbInstance() hook is
                ported, wired to that memo
  applied as-is:
    8778ea7d18  stamp dist/BUILD_SHA before the npm provenance gate (#11721)
    aa52351113  decouple the Bun image from the release manifest (#11724)
    925feb27b8  let the bun digest artifact be absent (#11740)
    b65ef333da  size the install-upgrade gate to a measured run
    0ce21232db  #11845 converge install/upgrade schemas (migration renumbered in
                the next commit: 163 collides with 163_radar_feed_cache_generated_at)
    b7c07edad8  #11855 install-upgrade gate on disk, not tmpfs
    8e2fb04329  #11864 drop *.nft.json from the npm tarball (413)
    dea6bb8b6b  #11877 publish npm from a hosted runner (provenance 422)
  handled by the sync script that follows (CHANGELOG protocol):
    b4ec7807ab  Release v3.8.50 — squash of content this branch already carries
    5458026c21 / c44c0a29e8  CHANGELOG aggregation, stats and top-25
  applied separately (its own commit, ten files):
    65e81158ab  #11088 Ollama capability routing — a 5,094-file squash from a
                stale base; only the Ollama files are the change

Every cherry-pick that touched a file this branch had also changed was
resolved by hand and re-run through the tests both sides own for it.
2026-08-28 14:21:44 -03:00

143 lines
5.4 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
// @ts-expect-error — plain .mjs gate script, no type declarations by design
import {
assertNoDiskExhaustion,
evaluateConvergence,
} from "../../scripts/check/check-install-upgrade.mjs";
/**
* The whole point of this gate is that the two directions of schema divergence are NOT
* equivalent, and the cheap symmetric check ("do the table sets match?") would either
* block every release on harmless residue or let a real upgrade bug through.
*
* Measured on 2026-07-30: a real 3.8.48 install on VPS .16 upgraded to 3.8.49 ended with
* 117 tables while a clean 3.8.49 install had 116 — the extra one being `cache_metrics`.
*/
test("converged schemas pass", () => {
const v = evaluateConvergence({
freshTables: ["a", "b", "c"],
upgradedTables: ["c", "b", "a"],
});
assert.equal(v.ok, true);
assert.deepEqual(v.failures, []);
assert.deepEqual(v.onlyFresh, []);
assert.deepEqual(v.onlyUpgraded, []);
});
test("a table only a CLEAN install creates is ALWAYS a failure — upgraders lack structure", () => {
const v = evaluateConvergence({
freshTables: ["a", "b", "new_feature_table"],
upgradedTables: ["a", "b"],
});
assert.equal(v.ok, false);
assert.deepEqual(v.onlyFresh, ["new_feature_table"]);
assert.match(v.failures[0], /CLEAN install creates but an UPGRADE does not/);
assert.match(v.failures[0], /new_feature_table/);
});
test("that direction cannot be silenced by the residual allowlist", () => {
const v = evaluateConvergence({
freshTables: ["a", "missing_on_upgrade"],
upgradedTables: ["a"],
// Even if someone lists it here, the dangerous direction must still fail.
residualAllowlist: { missing_on_upgrade: "please ignore me" },
});
assert.equal(v.ok, false);
assert.deepEqual(v.onlyFresh, ["missing_on_upgrade"]);
});
test("known residue (cache_metrics, the real 3.8.48→3.8.49 finding) passes but is reported", () => {
const v = evaluateConvergence({
freshTables: ["a", "b"],
upgradedTables: ["a", "b", "cache_metrics"],
residualAllowlist: { cache_metrics: "measured 2026-07-30 on VPS .16" },
});
assert.equal(v.ok, true, "allowlisted residue must not block a release");
assert.deepEqual(v.onlyUpgraded, ["cache_metrics"], "still surfaced so it stays visible");
assert.deepEqual(v.unknownResidue, []);
});
test("UNKNOWN residue fails — a new divergence must not hide behind the allowlist", () => {
const v = evaluateConvergence({
freshTables: ["a"],
upgradedTables: ["a", "cache_metrics", "surprise_table"],
residualAllowlist: { cache_metrics: "known" },
});
assert.equal(v.ok, false);
assert.deepEqual(v.unknownResidue, ["surprise_table"]);
assert.match(v.failures[0], /surprise_table/);
assert.doesNotMatch(
v.failures[0],
/cache_metrics/,
"the known one must not be re-reported as new"
);
});
test("both directions at once report both failures", () => {
const v = evaluateConvergence({
freshTables: ["shared", "only_fresh"],
upgradedTables: ["shared", "only_upgraded"],
});
assert.equal(v.ok, false);
assert.equal(v.failures.length, 2);
assert.deepEqual(v.onlyFresh, ["only_fresh"]);
assert.deepEqual(v.unknownResidue, ["only_upgraded"]);
});
test("accepts Sets as well as arrays (the gate passes Sets from sqlite_master)", () => {
const v = evaluateConvergence({
freshTables: new Set(["a", "b"]),
upgradedTables: new Set(["a", "b"]),
});
assert.equal(v.ok, true);
});
test("empty/missing inputs do not crash", () => {
const v = evaluateConvergence({ freshTables: undefined, upgradedTables: undefined });
assert.equal(v.ok, true);
assert.deepEqual(v.onlyFresh, []);
});
// ─── ENOSPC guard ──────────────────────────────────────────────────────────────
//
// The v3.8.50 publish run (CI 33104507735) failed with "15 tables a CLEAN install creates
// but an UPGRADE does not". None of them was missing: the Phase B upgrade install had hit
// `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` 5611 times, npm still
// exited 0, the truncated `omniroute serve` "exited with code 0 before serving", and the
// database therefore still held the 3.8.49 schema. npm reporting disk exhaustion as a
// warning is what let a full disk masquerade as a schema defect.
test("npm ENOSPC warnings are raised as an install failure, not ignored", () => {
const enospc = "npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device, write\n".repeat(3);
assert.throws(
() => assertNoDiskExhaustion(enospc, "upgrade install"),
(err: Error) => {
assert.match(err.message, /upgrade install/);
assert.match(err.message, /ran out of disk space/);
assert.match(err.message, /3 ENOSPC error/);
// The operator must not go looking for a migration that is not missing.
assert.match(err.message, /NOT a schema divergence/);
return true;
}
);
});
test("a clean install log does not trip the disk guard", () => {
assert.doesNotThrow(() =>
assertNoDiskExhaustion(
"npm warn deprecated boolean@3.2.0: Package no longer supported.\nadded 900 packages\n",
"clean install"
)
);
});
test("the guard also catches the bare kernel message without the ENOSPC code", () => {
assert.throws(
() => assertNoDiskExhaustion("Error: no space left on device", "clean install"),
/ran out of disk space/
);
});