104 lines
4.5 KiB
TypeScript
104 lines
4.5 KiB
TypeScript
/** Disposable inline fixtures only: never require a built payload or workflow artifacts. */
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { describe, it } from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
import { resolveSmokeOptions, validateSmokeOptions } from "../scripts/smoke-server.mjs";
|
|
|
|
const script = fileURLToPath(new URL("../scripts/smoke-server.mjs", import.meta.url));
|
|
const checkout = fileURLToPath(new URL("..", import.meta.url));
|
|
const run = (args: string[]) => spawnSync(process.execPath, [script, ...args], { encoding: "utf8", timeout: 10_000 });
|
|
|
|
function withFixture(test: (root: string) => void) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lh-server-options-test-"));
|
|
try { test(root); }
|
|
finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
}
|
|
|
|
function fails(args: string[], message: RegExp) {
|
|
const result = run(args);
|
|
assert.equal(result.error, undefined);
|
|
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
assert.match(result.stderr, message);
|
|
assert.doesNotMatch(result.stdout, /boot ok|smoke passed|import ok/);
|
|
}
|
|
|
|
describe("standalone server smoke options", () => {
|
|
it("preserves the checkout staging default and host runtime without booting on import", () => {
|
|
const options = resolveSmokeOptions();
|
|
assert.equal(options.standalone, path.join(checkout, ".next", "standalone"));
|
|
assert.equal(options.server, path.join(options.standalone, "server.js"));
|
|
assert.equal(options.runtime, process.execPath);
|
|
});
|
|
|
|
it("resolves explicit relative paths against the caller and accepts an explicit root", () => {
|
|
const relative = resolveSmokeOptions(["--standalone", "payload", "--runtime", "bin/runtime"]);
|
|
assert.equal(relative.standalone, path.resolve("payload"));
|
|
assert.equal(relative.runtime, path.resolve("bin/runtime"));
|
|
withFixture((root) => {
|
|
fs.writeFileSync(path.join(root, "server.js"), "// fixture; never booted");
|
|
const options = resolveSmokeOptions(["--runtime", process.execPath, "--standalone", root]);
|
|
assert.equal(options.server, path.join(root, "server.js"));
|
|
assert.doesNotThrow(() => validateSmokeOptions(options));
|
|
});
|
|
});
|
|
|
|
for (const option of ["--standalone", "--runtime"]) {
|
|
for (const suffix of [[], [""], ["--electron"]]) {
|
|
it(`rejects missing or malformed ${option} value ${JSON.stringify(suffix)}`, () => {
|
|
fails([option, ...suffix], new RegExp(`${option} requires a path`));
|
|
});
|
|
}
|
|
}
|
|
|
|
it("rejects unknown flags and conflicting runtime selectors", () => {
|
|
fails(["--unknown"], /Unknown option/);
|
|
fails(["--electron", "--runtime", process.execPath], /Use either --electron or --runtime/);
|
|
});
|
|
|
|
it("retains the legacy Electron runtime selector", () => {
|
|
assert.match(resolveSmokeOptions(["--electron"]).runtime, /electron/);
|
|
});
|
|
|
|
it("fails early for nonexistent and non-directory roots", () => {
|
|
withFixture((root) => {
|
|
fails(["--standalone", path.join(root, "absent")], /Standalone directory not found/);
|
|
const file = path.join(root, "file");
|
|
fs.writeFileSync(file, "fixture");
|
|
fails(["--standalone", file], /Standalone directory not found/);
|
|
});
|
|
});
|
|
|
|
it("fails early for missing or non-file server entries", () => {
|
|
withFixture((root) => {
|
|
fails(["--standalone", root], /Standalone server not found/);
|
|
fs.mkdirSync(path.join(root, "server.js"));
|
|
fails(["--standalone", root], /Standalone server not found/);
|
|
});
|
|
});
|
|
|
|
it("fails early for missing, non-file and non-executable runtimes", () => {
|
|
withFixture((root) => {
|
|
fs.writeFileSync(path.join(root, "server.js"), "// fixture");
|
|
fails(["--standalone", root, "--runtime", path.join(root, "missing")], /Runtime not found/);
|
|
fails(["--standalone", root, "--runtime", root], /Runtime not found/);
|
|
const runtime = path.join(root, "runtime");
|
|
fs.writeFileSync(runtime, "fixture", { mode: 0o600 });
|
|
fails(["--standalone", root, "--runtime", runtime], /EACCES/);
|
|
});
|
|
});
|
|
|
|
for (const relative of ["auth.json", ".env", ".env.local", ".env.production", ".env.production.local", ".learnharness", "data"]) {
|
|
it(`rejects leaked credential/data path ${relative} before dependency probes`, () => {
|
|
withFixture((root) => {
|
|
fs.writeFileSync(path.join(root, "server.js"), "// fixture");
|
|
fs.writeFileSync(path.join(root, relative), "not-a-real-credential");
|
|
fails(["--standalone", root], /Credential\/data path leaked into standalone/);
|
|
});
|
|
});
|
|
}
|
|
});
|