mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 03:02:14 +03:00
Merge remote-tracking branch 'upstream/release/v3.8.50' into feat/cursor-token-renewal
# Conflicts: # config/quality/eslint-suppressions.json
This commit is contained in:
@@ -116,6 +116,25 @@ const EXTRA_MODULE_ENTRIES = [
|
||||
{ label: "split2", src: ["node_modules", "split2"], dest: ["node_modules", "split2"] },
|
||||
{ label: "migrations", src: ["src", "lib", "db", "migrations"], dest: ["migrations"] },
|
||||
{ label: "MITM server", src: ["src", "mitm", "server.cjs"], dest: ["src", "mitm", "server.cjs"] },
|
||||
{
|
||||
// #9451: server.cjs requires 6 shims from ./_internal/ (bypass, ingest,
|
||||
// forwardTarget, aliasConfig, standaloneRouting, rootCaShim) which the MITM
|
||||
// child process loads via require(). Next.js's standalone tracer never sees
|
||||
// them (server.cjs is a separate node process, not imported by the main
|
||||
// server), so the _internal/ directory must be copied explicitly or the MITM
|
||||
// child crashes with MODULE_NOT_FOUND at boot.
|
||||
label: "MITM _internal shims (#9451)",
|
||||
src: ["src", "mitm", "_internal"],
|
||||
dest: ["src", "mitm", "_internal"],
|
||||
},
|
||||
{
|
||||
// #9451: rootCaShim.cjs does `await import("selfsigned")` for dynamic SSL
|
||||
// certificate generation. The MITM child is not traced by Next.js, so the
|
||||
// package is absent from the Docker standalone bundle without this entry.
|
||||
label: "selfsigned (MITM rootCaShim dynamic import — #9451)",
|
||||
src: ["node_modules", "selfsigned"],
|
||||
dest: ["node_modules", "selfsigned"],
|
||||
},
|
||||
{
|
||||
label: "run-standalone script",
|
||||
src: ["scripts", "dev", "run-standalone.mjs"],
|
||||
@@ -214,6 +233,11 @@ const EXTRA_MODULE_ENTRIES = [
|
||||
src: ["node_modules", "undici"],
|
||||
dest: ["node_modules", "undici"],
|
||||
},
|
||||
{
|
||||
label: "sql.js WASM fallback runtime",
|
||||
src: ["node_modules", "sql.js"],
|
||||
dest: ["node_modules", "sql.js"],
|
||||
},
|
||||
{
|
||||
label: "sqlite-vec wrapper (vector memory - loaded at runtime via createRequire)",
|
||||
src: ["node_modules", "sqlite-vec"],
|
||||
|
||||
@@ -209,6 +209,19 @@ export function normalizeArtifactPath(filePath: string): string {
|
||||
.replace(/\/{2,}/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Paths that are NEVER publishable, whatever the allowlist says.
|
||||
*
|
||||
* Existence reason: the allowlist grants whole prefixes (e.g.
|
||||
* `@omniroute/opencode-provider/`), so a nested `node_modules` inside an allowed
|
||||
* prefix used to be authorized by it. That shipped 79 MB of devDependencies
|
||||
* (tsup/esbuild/typescript) — 80% of the tarball — whenever the publish ran from
|
||||
* a machine where someone had installed inside that subpackage. `files[]` in
|
||||
* package.json now excludes it at the source; this is the gate that FAILS if it
|
||||
* ever comes back instead of silently allowing it.
|
||||
*/
|
||||
export const PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS: string[] = ["node_modules"];
|
||||
|
||||
export function findUnexpectedArtifactPaths(
|
||||
filePaths: string[],
|
||||
{ exactPaths = [], prefixPaths = [] }: { exactPaths?: string[]; prefixPaths?: string[] } = {}
|
||||
@@ -216,13 +229,17 @@ export function findUnexpectedArtifactPaths(
|
||||
const normalizedExact = new Set(exactPaths.map(normalizeArtifactPath));
|
||||
const normalizedPrefixes = prefixPaths.map(normalizeArtifactPath);
|
||||
|
||||
const hasForbiddenSegment = (filePath: string): boolean =>
|
||||
filePath.split("/").some((segment) => PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS.includes(segment));
|
||||
|
||||
return filePaths
|
||||
.map(normalizeArtifactPath)
|
||||
.filter(Boolean)
|
||||
.filter(
|
||||
(filePath) =>
|
||||
!normalizedExact.has(filePath) &&
|
||||
!normalizedPrefixes.some((prefix) => filePath.startsWith(prefix))
|
||||
hasForbiddenSegment(filePath) ||
|
||||
(!normalizedExact.has(filePath) &&
|
||||
!normalizedPrefixes.some((prefix) => filePath.startsWith(prefix)))
|
||||
)
|
||||
.sort();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// igual ao próprio teto ficava presa no baseline para sempre — ver #8584.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
@@ -22,6 +23,7 @@ const BASELINE_PATH = path.resolve(
|
||||
getArg("--baseline", path.join(ROOT, "config/quality/file-size-baseline.json"))
|
||||
);
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
const BASE_REF = getArg("--base-ref"); // SHA for PR base-relative mode (#8522)
|
||||
const SCAN_DIRS = ["src", "open-sse", "electron", "bin"];
|
||||
// Test files live under tests/ plus co-located *.test.ts(x) inside the source dirs.
|
||||
const TEST_SCAN_DIRS = ["tests", ...SCAN_DIRS];
|
||||
@@ -37,20 +39,39 @@ const SKIP_DIRS = new Set(["node_modules", "dist-electron", ".next", ".build", "
|
||||
* (loc < frozen), entao uma entrada igual ao proprio teto nunca saia da lista,
|
||||
* por mais abaixo do cap que estivesse (3 casos reais no v3.8.49).
|
||||
*
|
||||
* Quando `baseLocByFile` e fornecido (modo PR), a violacao e computada contra
|
||||
* o MAIOR entre o valor congelado e o valor na base -- assim um PR inocente
|
||||
* (head === base no arquivo) nao e penalizado por drift herdado (#8522).
|
||||
*
|
||||
* @param {Object} currentLocByFile — LOC atuais (head)
|
||||
* @param {Object} frozen — baseline congelado
|
||||
* @param {number} cap — teto para arquivos novos
|
||||
* @param {Object} [baseLocByFile] — LOC na branch base (opcional, modo PR)
|
||||
* @returns {{violations: string[], improvements: [string, number][], redundant: string[]}}
|
||||
*/
|
||||
export function evaluateFileSizes(currentLocByFile, frozen, cap) {
|
||||
export function evaluateFileSizes(currentLocByFile, frozen, cap, baseLocByFile) {
|
||||
const violations = [];
|
||||
const improvements = [];
|
||||
const redundant = [];
|
||||
for (const [file, loc] of Object.entries(currentLocByFile)) {
|
||||
if (file in frozen) {
|
||||
if (loc > frozen[file])
|
||||
const threshold = baseLocByFile
|
||||
? Math.max(frozen[file], baseLocByFile[file] ?? frozen[file])
|
||||
: frozen[file];
|
||||
if (loc > threshold)
|
||||
violations.push(`${file}: ${loc} > congelado ${frozen[file]} (não pode crescer)`);
|
||||
else if (loc < frozen[file]) improvements.push([file, loc]);
|
||||
else if (loc <= cap) redundant.push(file);
|
||||
} else if (loc > cap) {
|
||||
violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`);
|
||||
if (!baseLocByFile) {
|
||||
violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`);
|
||||
} else {
|
||||
// Modo PR: so viola se cresceu alem do que ja estava na base
|
||||
const baseLoc = baseLocByFile[file] ?? 0;
|
||||
const prThreshold = Math.max(cap, baseLoc);
|
||||
if (loc > prThreshold)
|
||||
violations.push(`${file}: ${loc} > cap ${cap} (arquivo novo acima do limite)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { violations, improvements, redundant };
|
||||
@@ -108,6 +129,30 @@ function collectTestLoc() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computa LOC por arquivo a partir de um ref git (branch, SHA, tag).
|
||||
* Usado pelo modo --base-ref para obter a contagem na base do PR (#8522).
|
||||
* @param {string} ref — git ref (e.g. SHA da branch base)
|
||||
* @param {string[]} files — lista de paths relativos ao ROOT
|
||||
* @returns {Object} mapa file → line count
|
||||
*/
|
||||
function getBaseLoc(ref, files) {
|
||||
const out = {};
|
||||
for (const file of files) {
|
||||
try {
|
||||
const buf = execFileSync("git", ["show", `${ref}:${file}`], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
timeout: 5000,
|
||||
});
|
||||
out[file] = buf.split("\n").length;
|
||||
} catch {
|
||||
// Arquivo nao existe na base (novo no PR) — tratado como 0
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(BASELINE_PATH)) {
|
||||
console.error(`[file-size] FAIL — ${path.basename(BASELINE_PATH)} ausente.`);
|
||||
@@ -117,7 +162,17 @@ function main() {
|
||||
const cap = baseline.cap;
|
||||
const frozen = baseline.frozen || {};
|
||||
const current = collectLoc();
|
||||
const { violations, improvements, redundant } = evaluateFileSizes(current, frozen, cap);
|
||||
|
||||
// Modo PR: computa LOC na branch base para comparacao relativa (#8522)
|
||||
const baseLoc = BASE_REF ? getBaseLoc(BASE_REF, Object.keys(current)) : undefined;
|
||||
if (BASE_REF) {
|
||||
const baseKeys = Object.keys(baseLoc).length;
|
||||
console.log(
|
||||
`[file-size] modo PR (--base-ref ${BASE_REF.slice(0, 12)}): ${baseKeys} arquivos da base computados`
|
||||
);
|
||||
}
|
||||
|
||||
const { violations, improvements, redundant } = evaluateFileSizes(current, frozen, cap, baseLoc);
|
||||
|
||||
// Test-file gate (Layer 1 anti-reinflation): same shrink-only + new-≤cap semantics,
|
||||
// reusing evaluateFileSizes against the testFrozen baseline + testCap.
|
||||
@@ -129,7 +184,7 @@ function main() {
|
||||
improvements: testImprovements,
|
||||
redundant: testRedundant,
|
||||
} = typeof testCap === "number"
|
||||
? evaluateFileSizes(currentTests, testFrozen, testCap)
|
||||
? evaluateFileSizes(currentTests, testFrozen, testCap, BASE_REF ? baseLoc : undefined)
|
||||
: { violations: [], improvements: [], redundant: [] };
|
||||
|
||||
if (UPDATE) {
|
||||
|
||||
@@ -20,6 +20,13 @@ import path from "node:path";
|
||||
|
||||
const POLL_INTERVAL_MS = 2_000;
|
||||
const BOOT_DEADLINE_MS = 240_000;
|
||||
const SQLJS_STARTUP_MARKER = "Pre-initializing sql.js WASM";
|
||||
|
||||
export const REQUIRED_SQLJS_RUNTIME_FILES = Object.freeze([
|
||||
"dist/node_modules/sql.js/package.json",
|
||||
"dist/node_modules/sql.js/dist/sql-wasm.js",
|
||||
"dist/node_modules/sql.js/dist/sql-wasm.wasm",
|
||||
]);
|
||||
|
||||
/** Parse `npm pack --json` output into the generated tarball filename. */
|
||||
export function pickTarball(packJsonOutput) {
|
||||
@@ -49,20 +56,278 @@ export function pickPort(seed = process.pid) {
|
||||
return 23000 + (seed % 4000);
|
||||
}
|
||||
|
||||
export function findMissingSqlJsRuntimeFiles(packageRoot, exists = fs.existsSync) {
|
||||
return REQUIRED_SQLJS_RUNTIME_FILES.filter(
|
||||
(relativePath) => !exists(path.join(packageRoot, relativePath))
|
||||
);
|
||||
}
|
||||
|
||||
export function evaluateSqlJsRoundTrip({
|
||||
startupOutput,
|
||||
beforeValue,
|
||||
patchedValue,
|
||||
readBackValue,
|
||||
}) {
|
||||
const failures = [];
|
||||
if (!startupOutput.includes(SQLJS_STARTUP_MARKER)) {
|
||||
failures.push("server output did not confirm the forced sql.js startup path");
|
||||
}
|
||||
if (patchedValue !== !beforeValue) {
|
||||
failures.push(
|
||||
`PATCH debugMode returned ${String(patchedValue)} (expected ${String(!beforeValue)})`
|
||||
);
|
||||
}
|
||||
if (readBackValue !== !beforeValue) {
|
||||
failures.push(
|
||||
`GET debugMode returned ${String(readBackValue)} (expected ${String(!beforeValue)})`
|
||||
);
|
||||
}
|
||||
return { ok: failures.length === 0, failures };
|
||||
}
|
||||
|
||||
/**
|
||||
* After a clean shutdown + restart with the same DATA_DIR, the value written in boot #1
|
||||
* must be read back from disk in boot #2. sql.js is in-memory with debounced/flush writes,
|
||||
* so this proves the persisted file actually landed and the restart reads it.
|
||||
*/
|
||||
export function evaluateRestartPersistence({ expectedValue, restartValue }) {
|
||||
const failures = [];
|
||||
if (restartValue !== expectedValue) {
|
||||
failures.push(
|
||||
`restart GET debugMode returned ${String(restartValue)} (expected ${String(expectedValue)} after restart)`
|
||||
);
|
||||
}
|
||||
return { ok: failures.length === 0, failures };
|
||||
}
|
||||
|
||||
async function readJsonResponse(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const body = await response.json().catch(() => null);
|
||||
return { response, body };
|
||||
}
|
||||
|
||||
async function verifySettingsRoundTrip(baseUrl, startupOutput) {
|
||||
const initial = await readJsonResponse(`${baseUrl}/api/settings`);
|
||||
if (initial.response.status !== 200 || !initial.body || typeof initial.body !== "object") {
|
||||
return {
|
||||
ok: false,
|
||||
failures: [`initial settings HTTP ${initial.response.status} or non-JSON body`],
|
||||
};
|
||||
}
|
||||
|
||||
const beforeValue = initial.body.debugMode === true;
|
||||
const expectedValue = !beforeValue;
|
||||
const patched = await readJsonResponse(`${baseUrl}/api/settings`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ debugMode: expectedValue }),
|
||||
});
|
||||
if (patched.response.status !== 200 || !patched.body || typeof patched.body !== "object") {
|
||||
return {
|
||||
ok: false,
|
||||
failures: [`settings PATCH HTTP ${patched.response.status} or non-JSON body`],
|
||||
};
|
||||
}
|
||||
|
||||
const readBack = await readJsonResponse(`${baseUrl}/api/settings`);
|
||||
if (readBack.response.status !== 200 || !readBack.body || typeof readBack.body !== "object") {
|
||||
return {
|
||||
ok: false,
|
||||
failures: [`settings read-back HTTP ${readBack.response.status} or non-JSON body`],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...evaluateSqlJsRoundTrip({
|
||||
startupOutput,
|
||||
beforeValue,
|
||||
patchedValue: patched.body.debugMode,
|
||||
readBackValue: readBack.body.debugMode,
|
||||
}),
|
||||
// The exact value boot #2 must read back from disk to prove persistence.
|
||||
expectedValue,
|
||||
};
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
console.log(`[pack-boot] ${msg}`);
|
||||
}
|
||||
|
||||
/** Node sets exitCode/signalCode synchronously when the process dies — authoritative. */
|
||||
function hasExited(child) {
|
||||
return child.exitCode !== null || child.signalCode !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* SIGTERM the process GROUP and wait for its REAL exit — the graceful-shutdown handler
|
||||
* (initGracefulShutdown) drains requests, checkpoints the DB via closeDbInstance(), then
|
||||
* calls process.exit(0). A fixed sleep + hard kill could SIGKILL mid-flush and silently
|
||||
* drop the very persistence this gate proves, so SIGKILL is a last resort after the grace
|
||||
* deadline, and a CONFIRMED exit is required before returning: if even SIGKILL fails to
|
||||
* reap, throw, so boot #2 cannot start against a port a zombie still holds.
|
||||
*
|
||||
* The child is spawned with detached:true, so it leads its own process group and
|
||||
* -child.pid signals the whole tree, not just the launcher.
|
||||
*/
|
||||
async function stopChild(child, graceMs = 30_000) {
|
||||
if (!child?.pid) return;
|
||||
// Fast path: already reaped (crashed mid-smoke, or exited before this call) — nothing
|
||||
// left to signal or wait for.
|
||||
if (hasExited(child)) return;
|
||||
|
||||
let onSettled;
|
||||
const exited = new Promise((resolve) => {
|
||||
onSettled = () => resolve();
|
||||
child.once("exit", onSettled);
|
||||
child.once("close", onSettled);
|
||||
});
|
||||
// Race the exit/close promise against a timeout; then re-read authoritative state, so a
|
||||
// same-tick exit that lost the race still counts. Timer is always cleared.
|
||||
const waitForExit = (ms) => {
|
||||
let timer;
|
||||
return Promise.race([
|
||||
exited,
|
||||
new Promise((resolve) => {
|
||||
timer = setTimeout(resolve, ms);
|
||||
}),
|
||||
])
|
||||
.finally(() => clearTimeout(timer))
|
||||
.then(() => hasExited(child));
|
||||
};
|
||||
|
||||
try {
|
||||
// Re-check AFTER attaching: if the process died in the gap between the fast path and
|
||||
// listener attach, once("exit") can never fire (event already emitted), and without
|
||||
// this waitForExit would burn the full grace window.
|
||||
if (hasExited(child)) return;
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, "SIGTERM");
|
||||
} catch {
|
||||
/* group already gone */
|
||||
}
|
||||
if (await waitForExit(graceMs)) return;
|
||||
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
} catch {
|
||||
/* group already gone */
|
||||
}
|
||||
if (!(await waitForExit(5_000))) {
|
||||
throw new Error(
|
||||
`[pack-boot] server process group ${child.pid} still alive 5s after SIGKILL — ` +
|
||||
"refusing to reboot on the same port"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
child.removeListener("exit", onSettled);
|
||||
child.removeListener("close", onSettled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the installed CLI once on an isolated DATA_DIR. The child is spawned detached:true
|
||||
* so it leads its own process group — stopChild() relies on that to SIGTERM the whole tree.
|
||||
* The caller owns shutdown so the graceful DB flush lands before teardown.
|
||||
*/
|
||||
function spawnServer(binPath, port, dataDir) {
|
||||
const child = spawn(binPath, ["serve", "--port", String(port), "--log", "--no-open"], {
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
DATA_DIR: dataDir,
|
||||
JWT_SECRET: "pack-boot-smoke-secret-with-sufficient-length-000",
|
||||
API_KEY_SECRET: "pack-boot-smoke-api-key-secret-long",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1",
|
||||
OMNIROUTE_PACK_BOOT_SMOKE: "1",
|
||||
OMNIROUTE_PACK_BOOT_FORCE_SQLJS: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
});
|
||||
const tail = [];
|
||||
const keepTail = (chunk) => {
|
||||
tail.push(String(chunk));
|
||||
while (tail.length > 80) tail.shift();
|
||||
};
|
||||
child.stdout.on("data", keepTail);
|
||||
child.stderr.on("data", keepTail);
|
||||
return { child, tail };
|
||||
}
|
||||
|
||||
/** Poll /api/monitoring/health until the packed version answers or the boot deadline passes. */
|
||||
async function waitForHealthy(port, child, expectedVersion) {
|
||||
// Seed from authoritative state (Node sets these synchronously at death), then attach a
|
||||
// named once-listener, then re-check: a child that died before this call, or in the gap
|
||||
// before the listener attached, would otherwise never fire "exit" and waste the deadline.
|
||||
const exitDescriptor = (code, signal) => (signal ? `signal ${signal}` : `code ${code ?? -1}`);
|
||||
let childExit = hasExited(child) ? exitDescriptor(child.exitCode, child.signalCode) : null;
|
||||
const onChildExit = (code, signal) => {
|
||||
childExit = exitDescriptor(code, signal);
|
||||
};
|
||||
child.once("exit", onChildExit);
|
||||
if (hasExited(child)) {
|
||||
childExit = exitDescriptor(child.exitCode, child.signalCode);
|
||||
}
|
||||
|
||||
const deadline = Date.now() + BOOT_DEADLINE_MS;
|
||||
let verdict = { ok: false, failures: ["never polled"] };
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
if (childExit !== null) {
|
||||
return { ok: false, failures: [`process exited (${childExit}) before serving`] };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`);
|
||||
const body = await res.json().catch(() => null);
|
||||
verdict = evaluateBoot(res.status, body, expectedVersion);
|
||||
if (verdict.ok) return verdict;
|
||||
} catch {
|
||||
// not listening yet — keep polling
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
||||
}
|
||||
return verdict;
|
||||
} finally {
|
||||
child.removeListener("exit", onChildExit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current debugMode setting and return the EXACT boolean. A missing or non-boolean
|
||||
* field throws: coercing with `=== true` would read `false` for a malformed response and
|
||||
* could falsely "pass" persistence whenever the expected value happens to be false.
|
||||
*/
|
||||
async function readSettingsDebugMode(baseUrl) {
|
||||
const { response, body } = await readJsonResponse(`${baseUrl}/api/settings`);
|
||||
if (response.status !== 200 || !body || typeof body !== "object") {
|
||||
throw new Error(`settings GET HTTP ${response.status} or non-JSON body`);
|
||||
}
|
||||
if (typeof body.debugMode !== "boolean") {
|
||||
throw new Error(`settings debugMode is ${typeof body.debugMode} (expected boolean)`);
|
||||
}
|
||||
return body.debugMode;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ROOT = process.cwd();
|
||||
if (!fs.existsSync(path.join(ROOT, "dist", "server.js"))) {
|
||||
console.error("[pack-boot] dist/server.js missing — run `npm run build:cli` first (this is a --with-build gate)");
|
||||
console.error(
|
||||
"[pack-boot] dist/server.js missing — run `npm run build:cli` first (this is a --with-build gate)"
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const expectedVersion = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
|
||||
const expectedVersion = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "package.json"), "utf8")
|
||||
).version;
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-pack-boot-"));
|
||||
let child = null;
|
||||
let tail = [];
|
||||
let exitCode = 1;
|
||||
let primaryError = null; // a smoke-logic failure: boot/PATCH/GET/restart, or an in-flow stop
|
||||
let cleanupError = null; // recorded ONLY in finally, ONLY for a final stopChild failure
|
||||
let shutdownConfirmed = false; // process group confirmed stopped → safe to rm the workspace
|
||||
try {
|
||||
log(`packing v${expectedVersion}…`);
|
||||
const packOut = execFileSync("npm", ["pack", "--json", "--pack-destination", tmp], {
|
||||
@@ -77,87 +342,116 @@ async function main() {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
const packageRoot = path.join(prefix, "lib", "node_modules", "omniroute");
|
||||
const missingSqlJsFiles = findMissingSqlJsRuntimeFiles(packageRoot);
|
||||
if (missingSqlJsFiles.length > 0) {
|
||||
throw new Error(
|
||||
`installed package is missing the sql.js runtime contract: ${missingSqlJsFiles.join(", ")}`
|
||||
);
|
||||
}
|
||||
log("installed package contains the complete sql.js WASM runtime");
|
||||
|
||||
const port = pickPort();
|
||||
const dataDir = path.join(tmp, "data");
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
const binPath = path.join(prefix, "bin", "omniroute");
|
||||
log(`booting installed CLI on :${port} (DATA_DIR isolated)…`);
|
||||
child = spawn(binPath, ["serve", "--port", String(port)], {
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
DATA_DIR: dataDir,
|
||||
JWT_SECRET: "pack-boot-smoke-secret-with-sufficient-length-000",
|
||||
API_KEY_SECRET: "pack-boot-smoke-api-key-secret-long",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
});
|
||||
const tail = [];
|
||||
const keepTail = (chunk) => {
|
||||
tail.push(String(chunk));
|
||||
while (tail.length > 80) tail.shift();
|
||||
};
|
||||
child.stdout.on("data", keepTail);
|
||||
child.stderr.on("data", keepTail);
|
||||
let childExit = null;
|
||||
child.on("exit", (code) => {
|
||||
childExit = code ?? -1;
|
||||
});
|
||||
|
||||
const deadline = Date.now() + BOOT_DEADLINE_MS;
|
||||
let verdict = { ok: false, failures: ["never polled"] };
|
||||
while (Date.now() < deadline) {
|
||||
if (childExit !== null) {
|
||||
verdict = { ok: false, failures: [`process exited with code ${childExit} before serving`] };
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`);
|
||||
const body = await res.json().catch(() => null);
|
||||
verdict = evaluateBoot(res.status, body, expectedVersion);
|
||||
if (verdict.ok) {
|
||||
log(`healthy: HTTP 200, version ${body.version}, status "${body.status}"`);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// not listening yet — keep polling
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
||||
}
|
||||
|
||||
// BOOT #1 — boot, prove the forced sql.js tier, PATCH a setting, then shut down cleanly
|
||||
// so the sql.js adapter's graceful persist actually lands on disk. The in-flow stopChild
|
||||
// THROWS on failure; that lands in catch as primaryError and boot #2 never starts.
|
||||
log(`boot #1: installed CLI on :${port} (DATA_DIR isolated)…`);
|
||||
({ child, tail } = spawnServer(binPath, port, dataDir));
|
||||
let verdict = await waitForHealthy(port, child, expectedVersion);
|
||||
if (verdict.ok) {
|
||||
log("✅ the packed tarball boots — #7065 class gate green");
|
||||
exitCode = 0;
|
||||
} else {
|
||||
console.error(`[pack-boot] ❌ boot FAILED: ${verdict.failures.join("; ")}`);
|
||||
console.error("[pack-boot] last server output:\n" + tail.join("").split("\n").slice(-40).join("\n"));
|
||||
log(`healthy: HTTP 200, version ${expectedVersion}`);
|
||||
const roundTrip = await verifySettingsRoundTrip(`http://127.0.0.1:${port}`, tail.join(""));
|
||||
if (roundTrip.ok) {
|
||||
log("settings write/read succeeded through the forced sql.js driver");
|
||||
await stopChild(child); // throws here → primaryError; boot #2 is skipped
|
||||
child = null;
|
||||
|
||||
// BOOT #2 — same DATA_DIR, fresh process: the value must be read back FROM DISK.
|
||||
log("boot #2: rebooting on the same DATA_DIR to prove disk persistence…");
|
||||
({ child, tail } = spawnServer(binPath, port, dataDir));
|
||||
verdict = await waitForHealthy(port, child, expectedVersion);
|
||||
if (verdict.ok) {
|
||||
log(`healthy: HTTP 200, version ${expectedVersion}`);
|
||||
const restartValue = await readSettingsDebugMode(`http://127.0.0.1:${port}`);
|
||||
const persistence = evaluateRestartPersistence({
|
||||
expectedValue: roundTrip.expectedValue,
|
||||
restartValue,
|
||||
});
|
||||
if (persistence.ok) {
|
||||
log("value survived a clean shutdown + restart — disk persistence proven");
|
||||
await stopChild(child); // throws here → primaryError
|
||||
child = null;
|
||||
exitCode = 0;
|
||||
} else {
|
||||
verdict = persistence;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
verdict = roundTrip;
|
||||
}
|
||||
}
|
||||
if (!verdict.ok) {
|
||||
primaryError = new Error(verdict.failures.join("; "));
|
||||
exitCode = 1;
|
||||
}
|
||||
} catch (e) {
|
||||
// Every smoke-logic failure — boot/PATCH/GET/restart AND in-flow stopChild throws.
|
||||
primaryError = e;
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
if (child?.pid) {
|
||||
// Tear down whatever is still running. This block records ONLY a stopChild failure,
|
||||
// and never overwrites primaryError.
|
||||
if (child) {
|
||||
try {
|
||||
process.kill(-child.pid, "SIGTERM");
|
||||
} catch {
|
||||
/* already gone */
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 2_000));
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
} catch {
|
||||
/* already gone */
|
||||
await stopChild(child);
|
||||
shutdownConfirmed = true;
|
||||
} catch (e) {
|
||||
cleanupError = e; // still !shutdownConfirmed → workspace preserved below
|
||||
}
|
||||
child = null;
|
||||
} else {
|
||||
// Stopped in-flow (already confirmed) or never spawned — nothing left to confirm.
|
||||
shutdownConfirmed = true;
|
||||
}
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
// Remove the workspace ONLY after confirmed shutdown; a process group that refused to
|
||||
// die keeps its DATA_DIR for diagnosis.
|
||||
if (shutdownConfirmed) {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Report primaryError as the smoke failure; report cleanupError separately. Either one
|
||||
// fails the gate.
|
||||
if (primaryError) {
|
||||
console.error(`[pack-boot] ❌ smoke FAILED: ${primaryError.message}`);
|
||||
if (tail.length) {
|
||||
console.error(
|
||||
"[pack-boot] last server output:\n" + tail.join("").split("\n").slice(-40).join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
if (cleanupError) {
|
||||
console.error(`[pack-boot] ❌ final shutdown FAILED: ${cleanupError.message}`);
|
||||
exitCode = 1;
|
||||
}
|
||||
if (exitCode === 0) {
|
||||
log("✅ the packed tarball boots AND persists — #7065 class gate green");
|
||||
}
|
||||
if (!shutdownConfirmed) {
|
||||
console.error(
|
||||
`[pack-boot] ⚠ process group not confirmed stopped — workspace preserved for diagnosis: ${tmp}`
|
||||
);
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
const isDirectRun =
|
||||
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
|
||||
if (isDirectRun) {
|
||||
main().catch((e) => {
|
||||
console.error("[pack-boot] fatal:", e.message);
|
||||
|
||||
@@ -106,9 +106,8 @@ function normalizeWhitespace(s) {
|
||||
*/
|
||||
export function countSignificantTokens(cond) {
|
||||
const tokens =
|
||||
(cond || "").match(
|
||||
/===|!==|==|!=|>=|<=|&&|\|\||[<>+\-*/%!]|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?/g
|
||||
) || [];
|
||||
(cond || "").match(/===|!==|==|!=|>=|<=|&&|\|\||[<>+\-*/%!]|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?/g) ||
|
||||
[];
|
||||
let count = 0;
|
||||
for (const tk of tokens) {
|
||||
if (/^[A-Za-z_$]/.test(tk)) {
|
||||
@@ -178,8 +177,7 @@ export function extractProdConditions(src) {
|
||||
}
|
||||
|
||||
// Comparison-bearing ternaries: `<lhs> <cmp> <rhs> ? … : …` (best-effort, low-noise).
|
||||
const ternRe =
|
||||
/([A-Za-z_$][\w$).\]]*\s*(?:===|!==|==|!=|>=|<=|>|<)\s*[^?;{}\n]+?)\s*\?/g;
|
||||
const ternRe = /([A-Za-z_$][\w$).\]]*\s*(?:===|!==|==|!=|>=|<=|>|<)\s*[^?;{}\n]+?)\s*\?/g;
|
||||
let t;
|
||||
while ((t = ternRe.exec(src))) {
|
||||
pushCond(t[1], ownerAt(t.index));
|
||||
@@ -199,7 +197,10 @@ export function extractImports(src) {
|
||||
if (!src) return names;
|
||||
const addModule = (mod) => {
|
||||
names.add(mod);
|
||||
const base = mod.split("/").pop().replace(/\.\w+$/, "");
|
||||
const base = mod
|
||||
.split("/")
|
||||
.pop()
|
||||
.replace(/\.\w+$/, "");
|
||||
if (base) names.add(base);
|
||||
};
|
||||
let m;
|
||||
@@ -227,8 +228,7 @@ export function extractImports(src) {
|
||||
export function findReimplementedConditions(prodSources, testSource, testImports) {
|
||||
const flags = [];
|
||||
if (!testSource) return flags;
|
||||
const imports =
|
||||
testImports instanceof Set ? testImports : new Set(testImports || []);
|
||||
const imports = testImports instanceof Set ? testImports : new Set(testImports || []);
|
||||
const squash = (s) => (s || "").replace(/\s+/g, "");
|
||||
const testSq = squash(testSource);
|
||||
const seen = new Set();
|
||||
@@ -251,10 +251,15 @@ export function findReimplementedConditions(prodSources, testSource, testImports
|
||||
* (filtro D do git diff --diff-filter=MDR).
|
||||
*
|
||||
* `deletionAllowlist` (`_deletedWithReplacement` no test-masking-allowlist.json)
|
||||
* isenta uma deleção SOMENTE quando o substituto declarado existe no HEAD e é
|
||||
* ele próprio um arquivo de teste — o caso "reescrito em outro path sem rename
|
||||
* detectável" (conteúdo novo demais para o -M do git). Qualquer entrada cujo
|
||||
* substituto não exista ou não seja teste continua flagada.
|
||||
* isenta uma deleção de duas formas, cada uma com sua própria verificação:
|
||||
* 1. `replacement` (path string) — o substituto declarado existe no HEAD e é
|
||||
* ele próprio um arquivo de teste — o caso "reescrito em outro path sem
|
||||
* rename detectável" (conteúdo novo demais para o -M do git).
|
||||
* 2. `sourceRemoved` (array de paths) — feature removida por completo: TODOS
|
||||
* os arquivos de produção listados precisam estar ausentes no HEAD (sem
|
||||
* substituto porque não há mais código a testar). Usar apenas quando a
|
||||
* remoção do código-fonte está confirmada na mesma commit/PR.
|
||||
* Qualquer entrada cuja condição declarada não se verifique continua flagada.
|
||||
*/
|
||||
export function evaluateDeletedFiles(
|
||||
deletedPaths,
|
||||
@@ -272,6 +277,14 @@ export function evaluateDeletedFiles(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (entry && Array.isArray(entry.sourceRemoved) && entry.sourceRemoved.length > 0) {
|
||||
const stillPresent = entry.sourceRemoved.filter((p) => fileExists(p));
|
||||
if (stillPresent.length === 0) continue;
|
||||
flags.push(
|
||||
`${f}: deleção allowlistada como feature removida mas ${stillPresent.join(", ")} ainda existe(m) no HEAD`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
flags.push(
|
||||
`${f}: arquivo de teste deletado — revisão humana obrigatória (mascaramento alto-sinal)`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user