mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
check:pack-artifact and check:pack-boot have been self-contradictory since 05/08, blocking the v3.8.50 publish in ci.yml (build:cli job) and npm-publish.yml: - check:pack-artifact FAILS any tarball path containing a node_modules segment (PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS; files[] also excludes "!**/node_modules/**"). - check:pack-boot REQUIRED sql.js under the vendored dist/node_modules/sql.js location — a path the tarball can never carry, so both gates could never be green at once. The packaging model is now dependency-based: sql.js and node-machine-id are declared `dependencies` (a clean install places them under <packageRoot>/node_modules/), and better-sqlite3 is an optionalDependency installed natively per platform (^13.0.2 — which also covers the darwin-arm64 prebuild gap from #11242 by construction). The runtime already resolves the WASM at <cwd>/node_modules/sql.js/dist/sql-wasm.wasm (src/lib/db/adapters/sqljsAdapter.ts). Changes: - scripts/check/check-pack-boot.mjs: REQUIRED_SQLJS_RUNTIME_FILES now points at node_modules/sql.js/{package.json,dist/sql-wasm.js, dist/sql-wasm.wasm} — the dependency-installed location the clean-prefix install actually produces. REQUIRED_MACHINE_TOKEN_RUNTIME_FILES was already correct and is unchanged. - bin/cli/runtime/sqliteRuntime.mjs: BETTER_SQLITE3_VERSION bumped ^12.10.1 -> ^13.0.2 to match optionalDependencies (the lazy runtime install was pulling the wrong major), and exported for the guard. - tests/unit/pack-boot-runtime-paths.test.ts (new, TDD: RED -> GREEN): pins that (a) no pack-boot required path references a never-publishable vendored dist/<segment> location (driven by PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS), (b) sql.js/node-machine-id stay declared dependencies, (c) the lazy-install spec stays on the declared optionalDependency major. - tests/unit/check-pack-boot.test.ts: the sql.js contract test pinned the old vendored path; updated to node_modules/sql.js/dist/sql-wasm.wasm. This is alignment to the real new contract (vendoring ended), not masking — the same test still asserts the find-missing behavior. Electron is unaffected: the vendored dist/node_modules bundle still exists for Electron packaging (postinstall.mjs and assembleStandalone.mjs untouched). Refs #11242 Refs #10296 Co-authored-by: Xiangzhe <bakryun0718@proton.me>
247 lines
9.2 KiB
TypeScript
247 lines
9.2 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createHmac } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
REQUIRED_SQLJS_RUNTIME_FILES,
|
|
REQUIRED_MACHINE_TOKEN_RUNTIME_FILES,
|
|
pickTarball,
|
|
evaluateBoot,
|
|
pickPort,
|
|
findMissingSqlJsRuntimeFiles,
|
|
findMissingMachineTokenRuntimeFiles,
|
|
evaluateMachineTokenAuth,
|
|
evaluateSqlJsRoundTrip,
|
|
evaluateRestartPersistence,
|
|
} from "../../scripts/check/check-pack-boot.mjs";
|
|
|
|
// WS1.2 (T1, v3.8.49 quality plan) — pure-function guards for the tarball boot-smoke
|
|
// gate that kills the #7065 class (published artifact crashes on every boot because a
|
|
// packaging list drifted; 3rd recurrence). The end-to-end path runs in CI's
|
|
// package-artifact job; these tests pin the decision logic.
|
|
|
|
const SCRIPT_PATH = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
"../../scripts/check/check-pack-boot.mjs"
|
|
);
|
|
|
|
test("pickTarball extracts the filename from npm pack --json output", () => {
|
|
assert.equal(
|
|
pickTarball('[{"filename":"omniroute-3.8.49.tgz","size":1}]'),
|
|
"omniroute-3.8.49.tgz"
|
|
);
|
|
});
|
|
|
|
test("pickTarball normalizes scoped slashes to the on-disk dash form", () => {
|
|
assert.equal(pickTarball('[{"filename":"@scope/pkg-1.0.0.tgz"}]'), "@scope-pkg-1.0.0.tgz");
|
|
});
|
|
|
|
test("pickTarball throws on empty/odd npm output instead of booting garbage", () => {
|
|
assert.throws(() => pickTarball("[]"));
|
|
assert.throws(() => pickTarball("{}"));
|
|
});
|
|
|
|
test("evaluateBoot passes on HTTP 200 + matching version, whatever the health status", () => {
|
|
const r = evaluateBoot(200, { version: "3.8.49", status: "warning" }, "3.8.49");
|
|
assert.equal(r.ok, true);
|
|
assert.deepEqual(r.failures, []);
|
|
});
|
|
|
|
test("evaluateBoot fails on non-200, non-JSON body, and version mismatch", () => {
|
|
assert.equal(evaluateBoot(503, { version: "3.8.49" }, "3.8.49").ok, false);
|
|
assert.equal(evaluateBoot(200, null, "3.8.49").ok, false);
|
|
const wrong = evaluateBoot(200, { version: "3.8.48" }, "3.8.49");
|
|
assert.equal(wrong.ok, false);
|
|
assert.match(wrong.failures[0], /3\.8\.48/);
|
|
});
|
|
|
|
test("pickPort stays inside the reserved smoke range for any pid", () => {
|
|
for (const seed of [0, 1, 4000, 65535, 123456]) {
|
|
const p = pickPort(seed);
|
|
assert.ok(p >= 23000 && p < 27000, `port ${p} out of range for seed ${seed}`);
|
|
}
|
|
});
|
|
|
|
test("installed package contract requires sql.js metadata, entrypoint, and WASM", () => {
|
|
const present = new Set(REQUIRED_SQLJS_RUNTIME_FILES.map((file) => path.join("/pkg", file)));
|
|
assert.deepEqual(
|
|
findMissingSqlJsRuntimeFiles("/pkg", (file) => present.has(file)),
|
|
[]
|
|
);
|
|
|
|
// Dependency-based packaging (#11242): sql.js is a declared dependency, so the
|
|
// contract path is the npm-installed <packageRoot>/node_modules/sql.js location,
|
|
// never the old vendored dist/node_modules one (banned from the tarball).
|
|
present.delete(path.join("/pkg", "node_modules/sql.js/dist/sql-wasm.wasm"));
|
|
assert.deepEqual(
|
|
findMissingSqlJsRuntimeFiles("/pkg", (file) => present.has(file)),
|
|
["node_modules/sql.js/dist/sql-wasm.wasm"]
|
|
);
|
|
});
|
|
|
|
test("installed package contract requires a resolvable node-machine-id CommonJS runtime", () => {
|
|
const present = new Set(
|
|
REQUIRED_MACHINE_TOKEN_RUNTIME_FILES.map((file) => path.join("/pkg", file))
|
|
);
|
|
assert.deepEqual(
|
|
findMissingMachineTokenRuntimeFiles("/pkg", (file) => present.has(file)),
|
|
[]
|
|
);
|
|
|
|
present.delete(path.join("/pkg", "node_modules/node-machine-id/index.js"));
|
|
assert.deepEqual(
|
|
findMissingMachineTokenRuntimeFiles("/pkg", (file) => present.has(file)),
|
|
["node_modules/node-machine-id/index.js"]
|
|
);
|
|
});
|
|
|
|
test("machine-token smoke requires no/invalid credentials to fail and the packaged CLI token to pass", () => {
|
|
assert.deepEqual(
|
|
evaluateMachineTokenAuth({
|
|
cliToken: "a".repeat(64),
|
|
unauthenticatedStatus: 401,
|
|
invalidStatus: 401,
|
|
authenticatedStatus: 200,
|
|
}),
|
|
{ ok: true, failures: [] }
|
|
);
|
|
|
|
for (const candidate of [
|
|
{ cliToken: "", unauthenticatedStatus: 401, invalidStatus: 401, authenticatedStatus: 200 },
|
|
{
|
|
cliToken: "a".repeat(64),
|
|
unauthenticatedStatus: 200,
|
|
invalidStatus: 401,
|
|
authenticatedStatus: 200,
|
|
},
|
|
{
|
|
cliToken: "a".repeat(64),
|
|
unauthenticatedStatus: 401,
|
|
invalidStatus: 200,
|
|
authenticatedStatus: 200,
|
|
},
|
|
{
|
|
cliToken: "a".repeat(64),
|
|
unauthenticatedStatus: 401,
|
|
invalidStatus: 401,
|
|
authenticatedStatus: 401,
|
|
},
|
|
{
|
|
cliToken: createHmac("sha256", "").update("omniroute-cli-auth-v1").digest("hex"),
|
|
unauthenticatedStatus: 401,
|
|
invalidStatus: 401,
|
|
authenticatedStatus: 200,
|
|
},
|
|
]) {
|
|
assert.equal(evaluateMachineTokenAuth(candidate).ok, false);
|
|
}
|
|
});
|
|
|
|
test("sql.js round trip requires the forced-driver marker plus PATCH and GET persistence", () => {
|
|
const passing = evaluateSqlJsRoundTrip({
|
|
startupOutput: "[DB] Pre-initializing sql.js WASM (synchronous drivers unavailable)...",
|
|
beforeValue: true,
|
|
patchedValue: false,
|
|
readBackValue: false,
|
|
});
|
|
assert.deepEqual(passing, { ok: true, failures: [] });
|
|
|
|
const failing = evaluateSqlJsRoundTrip({
|
|
startupOutput: "[DB] SQLite database ready",
|
|
beforeValue: false,
|
|
patchedValue: true,
|
|
readBackValue: false,
|
|
});
|
|
assert.equal(failing.ok, false);
|
|
assert.equal(failing.failures.length, 2);
|
|
assert.match(failing.failures[0], /forced sql\.js startup path/);
|
|
assert.match(failing.failures[1], /GET debugMode/);
|
|
});
|
|
|
|
test("source guard: the gate polls the real health endpoint of the INSTALLED binary", () => {
|
|
const src = readFileSync(SCRIPT_PATH, "utf8");
|
|
assert.ok(
|
|
src.includes('"install", "-g", "--prefix"'),
|
|
"must install the packed tarball into a clean prefix"
|
|
);
|
|
assert.ok(src.includes("/api/monitoring/health"), "must poll the health endpoint");
|
|
assert.ok(src.includes("/api/settings"), "must verify a real application write and read");
|
|
assert.ok(src.includes("/api/cli/whoami"), "must exercise the machine-token auth endpoint");
|
|
assert.ok(src.includes("x-omniroute-cli-token"), "must send the official machine-token header");
|
|
const postinstall = readFileSync(
|
|
fileURLToPath(new URL("../../scripts/build/postinstall.mjs", import.meta.url)),
|
|
"utf8"
|
|
);
|
|
assert.ok(postinstall.includes('["sql.js", "node-machine-id"]'));
|
|
assert.ok(postinstall.includes('join(ROOT, "dist", "node_modules", packageName)'));
|
|
assert.ok(
|
|
src.includes('OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1"'),
|
|
"must force the packaged sql.js tier during this smoke"
|
|
);
|
|
assert.ok(src.includes("MAX_SERVER_OUTPUT_CHARS"));
|
|
assert.ok(!src.includes("while (tail.length > 80)"), "must not discard early startup proof");
|
|
assert.ok(src.indexOf("npm") < src.indexOf("spawn"), "pack+install must precede the boot spawn");
|
|
});
|
|
|
|
test("restart persistence requires the reboot value to match the boot #1 written value", () => {
|
|
assert.deepEqual(evaluateRestartPersistence({ expectedValue: true, restartValue: true }), {
|
|
ok: true,
|
|
failures: [],
|
|
});
|
|
assert.deepEqual(evaluateRestartPersistence({ expectedValue: false, restartValue: false }), {
|
|
ok: true,
|
|
failures: [],
|
|
});
|
|
|
|
const mismatch = evaluateRestartPersistence({ expectedValue: true, restartValue: false });
|
|
assert.equal(mismatch.ok, false);
|
|
assert.equal(mismatch.failures.length, 1);
|
|
assert.match(mismatch.failures[0], /after restart/);
|
|
});
|
|
|
|
test("source guard: the gate reboots on the SAME DATA_DIR and reads debugMode as a strict boolean", () => {
|
|
const src = readFileSync(SCRIPT_PATH, "utf8");
|
|
assert.ok(src.includes("boot #2"), "must run a second boot to prove disk persistence");
|
|
|
|
// Count only the CALLS, not the `function spawnServer(` declaration: the calls are the
|
|
// destructuring-assignment form `= spawnServer(...)`. Capture each call's arg list and
|
|
// assert both pass the SAME shared dataDir variable — that is what makes boot #2 read
|
|
// boot #1's disk state.
|
|
const calls = [...src.matchAll(/= spawnServer\(([^)]*)\)/g)];
|
|
assert.equal(calls.length, 2, "must spawn exactly two boots (write, then reboot to verify)");
|
|
for (const call of calls) {
|
|
assert.equal(
|
|
call[1],
|
|
"binPath, port, dataDir",
|
|
"both boots must pass the same shared dataDir variable"
|
|
);
|
|
}
|
|
|
|
assert.ok(
|
|
src.includes("evaluateRestartPersistence"),
|
|
"must evaluate the value read back after the reboot"
|
|
);
|
|
// readSettingsDebugMode must reject a missing/malformed field instead of coercing it, or
|
|
// a false expectedValue could pass on an empty response.
|
|
assert.ok(
|
|
src.includes('typeof body.debugMode !== "boolean"'),
|
|
"must require debugMode to be a real boolean, not coerce it"
|
|
);
|
|
});
|
|
|
|
test("source guard: final shutdown only deletes the workspace after a CONFIRMED stop", () => {
|
|
const src = readFileSync(SCRIPT_PATH, "utf8");
|
|
assert.ok(
|
|
src.includes("shutdownConfirmed"),
|
|
"must gate temp-dir deletion on a confirmed process-group stop"
|
|
);
|
|
assert.ok(src.includes("primaryError"), "must report the smoke failure distinctly");
|
|
assert.ok(src.includes("cleanupError"), "must report a final-shutdown failure distinctly");
|
|
assert.ok(
|
|
src.includes("hasExited(child)"),
|
|
"stopChild/waitForHealthy must read authoritative exit state, not a stale boolean"
|
|
);
|
|
});
|