feat: require explicit browser provider consent

This commit is contained in:
Sinabina
2026-07-21 12:07:01 -07:00
parent 75b3576670
commit e3effb3fc4
48 changed files with 2798 additions and 220 deletions
+199
View File
@@ -5,6 +5,7 @@ import path from "node:path";
import { spawn } from "node:child_process";
import { pathToFileURL } from "node:url";
import { main as runtimeMain } from "../runtime/cli.js";
import { configSetBrowserChoice } from "../runtime/config.js";
import { summarizeRuntimeBundle } from "../scripts/gstack2/audit-runtime-bundle";
import {
DEFAULT_CAPABILITY_LAUNCHERS,
@@ -14,6 +15,7 @@ import {
defaultBunBuilder,
installManagedRuntime,
normalizeManagedBrowserTree,
runInstallerCli,
runtimeReleaseComponentForPath,
runtimeNativePackagePaths,
uninstallManagedRuntime,
@@ -28,6 +30,12 @@ const ENTRIES = [
{ path: "cap/tool", build: "fixture", executable: true },
];
const CAPABILITIES = { "fixture-tool": "cap/tool" };
const BROWSER_ENTRIES = [
{ path: "runtime" },
{ path: "bin/gstack", executable: true },
{ path: "browse/dist/browse", build: "fixture", executable: true },
];
const BROWSER_CAPABILITIES = { browse: "browse/dist/browse" };
const REPO_ROOT = path.resolve(import.meta.dir, "..");
const FULL_RUNTIME_TEST_TIMEOUT_MS = process.platform === "win32" ? 120_000 : 30_000;
@@ -113,6 +121,185 @@ describe("GStack 2 managed runtime installer", () => {
}, { createDefaultSource: false });
});
test("stable launchers inject the persisted installed-browser choice and honor clearing it", async () => {
await withFixture(async ({ source, home }) => {
await installFixture(source, home, "browser-config-launcher", {
entries: BROWSER_ENTRIES,
capabilities: BROWSER_CAPABILITIES,
});
const executable = await fs.realpath(process.execPath);
await configSetBrowserChoice(home, { provider: "installed", executablePath: executable });
const ambient = { ...process.env, GSTACK_CHROMIUM_PATH: path.join(home, "ambient-browser-must-not-win") };
const selected = await runInstalledLauncher(home, "browse", [], { capture: true, env: ambient });
expect(selected.stdout).toBe(executable);
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
await expect(runInstalledLauncher(home, "browse", [], { capture: true, env: ambient }))
.rejects.toThrow("Command failed");
await configSetBrowserChoice(home, null);
await expect(runInstalledLauncher(home, "browse", [], { capture: true, env: ambient }))
.rejects.toThrow("Command failed");
});
});
test("installed-browser launchers refuse visible commands before starting the browser binary", async () => {
await withFixture(async ({ source, home }) => {
const executable = await fs.realpath(process.execPath);
await installFixture(source, home, "installed-visible-refusal", {
entries: BROWSER_ENTRIES,
capabilities: BROWSER_CAPABILITIES,
browserChoice: { provider: "installed", executablePath: executable },
});
await configSetBrowserChoice(home, { provider: "installed", executablePath: executable });
await expect(runInstalledLauncher(home, "browse", ["connect"], { capture: true }))
.rejects.toMatchObject({ stderr: expect.stringContaining("Visible GStack Browser requires managed Chromium") });
await expect(runInstalledLauncher(home, "browse", ["pair-agent"], { capture: true }))
.rejects.toMatchObject({ stderr: expect.stringContaining("Visible GStack Browser requires managed Chromium") });
await expect(runInstalledLauncher(home, "browse", ["handoff"], { capture: true }))
.rejects.toMatchObject({ stderr: expect.stringContaining("Visible GStack Browser requires managed Chromium") });
});
});
test("design-only launchers do not require an unrelated browser selection", async () => {
await withFixture(async ({ source, home }) => {
const design = path.join(source, "design", "dist", "design");
await fs.mkdir(path.dirname(design), { recursive: true });
await fs.writeFile(design, "#!/bin/sh\nprintf 'design ready\\n'\n", { mode: 0o755 });
await installManagedRuntime({
sourceDir: source,
home,
version: "design-without-browser",
entries: [...ENTRIES, { path: "design/dist/design", build: "fixture", executable: true }],
capabilities: { ...CAPABILITIES, "gstack-design": "design/dist/design" },
});
await configSetBrowserChoice(home, null);
expect((await runInstalledLauncher(home, "gstack-design", [], { capture: true })).stdout)
.toContain("design ready");
});
});
test("browser config refuses a provider that does not match the active runtime slot", async () => {
await withFixture(async ({ source, home }) => {
const result = await installFixture(source, home, "installed-slot");
const manifestPath = path.join(result.path, ".gstack-bundle.json");
const manifest = await readJson(manifestPath);
await fs.writeFile(manifestPath, JSON.stringify({
...manifest,
selectedCapabilities: ["browser"],
runtimeComponents: ["browser-code", "core"],
browserChoice: { provider: "installed", executablePath: process.execPath },
}));
const output = captureStream();
expect(await runtimeMain(["config", "browser", "managed"], {
cwd: source,
env: { ...process.env, GSTACK_HOME: home },
stdout: output.stream,
stderr: output.stream,
})).toBe(1);
expect(output.value()).toContain("active runtime was installed for installed Chromium");
const selected = captureStream();
expect(await runtimeMain(["config", "browser", "installed", process.execPath], {
cwd: source,
env: { ...process.env, GSTACK_HOME: home },
stdout: selected.stream,
stderr: selected.stream,
})).toBe(0);
expect((await readJson(path.join(home, "config.json"))).browser.provider).toBe("installed");
});
});
test("rollback validates a recorded installed browser before switching runtime slots", async () => {
await withFixture(async ({ root, source, home }) => {
const fallback = await installFixture(source, home, "installed-fallback");
const staleBrowser = path.join(root, "removed-chromium");
const fallbackManifestPath = path.join(fallback.path, ".gstack-bundle.json");
const fallbackManifest = await readJson(fallbackManifestPath);
await fs.writeFile(fallbackManifestPath, JSON.stringify({
...fallbackManifest,
selectedCapabilities: ["browser"],
runtimeComponents: ["browser-code", "core"],
browserChoice: { provider: "installed", executablePath: staleBrowser },
}));
const current = await installFixture(source, home, "managed-current");
const currentManifestPath = path.join(current.path, ".gstack-bundle.json");
const currentManifest = await readJson(currentManifestPath);
await fs.writeFile(currentManifestPath, JSON.stringify({
...currentManifest,
selectedCapabilities: ["browser"],
runtimeComponents: ["browser-headless", "core"],
browserChoice: { provider: "managed", executablePath: null },
}));
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
const output = captureStream();
expect(await runtimeMain(["upgrade", "--rollback"], {
cwd: source,
env: { ...process.env, GSTACK_HOME: home },
stdout: output.stream,
stderr: output.stream,
})).toBe(1);
expect(output.value()).toContain("unavailable or not executable");
expect((await readJson(path.join(home, "versions", "current.json"))).current).toBe("managed-current");
expect((await readJson(path.join(home, "config.json"))).browser)
.toEqual({ provider: "managed", executablePath: null });
});
});
test("an installed-browser setup persists the choice only after activation and launches through it", async () => {
await withFixture(async ({ root, source, home }) => {
await fs.writeFile(path.join(source, "cap", "tool"), `#!/usr/bin/env node
process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
`, { mode: 0o755 });
const executable = await fs.realpath(process.execPath);
const output = captureStream();
expect(await runInstallerCli([
"--source", source,
"--home", home,
"--capabilities", "browser",
"--browser", "installed",
"--browser-path", executable,
"--install-now",
"--yes",
"--json",
], {
stdout: output.stream,
stderr: output.stream,
prepareDependencies: async () => {},
installOptions: { entries: BROWSER_ENTRIES, capabilities: BROWSER_CAPABILITIES },
})).toBe(0);
expect((await readJson(path.join(home, "config.json"))).browser)
.toEqual({ provider: "installed", executablePath: executable });
expect((await runInstalledLauncher(home, "browse", [], { capture: true })).stdout)
.toBe(executable);
const failedHome = path.join(root, "failed-home", ".gstack");
const failed = captureStream();
expect(await runInstallerCli([
"--source", source,
"--home", failedHome,
"--capabilities", "browser",
"--browser", "installed",
"--browser-path", executable,
"--install-now",
"--yes",
"--json",
], {
stdout: failed.stream,
stderr: failed.stream,
prepareDependencies: async () => {},
installOptions: {
entries: BROWSER_ENTRIES,
capabilities: BROWSER_CAPABILITIES,
smokeTest: async () => { throw new Error("fixture smoke failure"); },
},
})).toBe(1);
expect(await exists(path.join(failedHome, "config.json"))).toBe(false);
});
});
test("accepts a symlink to the source root but rejects links inside the allowlist", async () => {
if (process.platform === "win32") return;
await withFixture(async ({ root, source, home }) => {
@@ -417,6 +604,13 @@ describe("GStack 2 managed runtime installer", () => {
await fs.writeFile(path.join(browserRoot, "chromium-fixture", "chrome"), "fixture\n", { mode: 0o755 });
return { code: 0, stdout: "", stderr: "" };
}
const outfileIndex = args.indexOf("--outfile");
if (outfileIndex >= 0 && typeof args[outfileIndex + 1] === "string") {
const outfile = path.join(REPO_ROOT, args[outfileIndex + 1]);
await fs.mkdir(path.dirname(outfile), { recursive: true });
await fs.writeFile(outfile, "fixture runtime helper\n", { mode: 0o755 });
return { code: 0, stdout: "", stderr: "" };
}
if (args[0] === "--version" && (command === process.execPath || command.includes(".gstack-runtime-tools"))) {
return { code: 0, stdout: "1.3.14\n", stderr: "" };
}
@@ -1095,15 +1289,20 @@ async function createSource(source: string) {
await fs.mkdir(path.join(source, "runtime"), { recursive: true });
await fs.mkdir(path.join(source, "bin"), { recursive: true });
await fs.mkdir(path.join(source, "cap"), { recursive: true });
await fs.mkdir(path.join(source, "browse", "dist"), { recursive: true });
await fs.writeFile(path.join(source, "package.json"), '{"name":"gstack","version":"2.0.0","type":"module"}\n');
await fs.writeFile(path.join(source, "runtime", "cli.js"), fixtureCli(""));
await fs.writeFile(path.join(source, "runtime", "tooling.js"),
'export async function resolveBashCommand(env = process.env) { return env.GSTACK_BASH || "bash"; }\n');
await fs.copyFile(path.join(REPO_ROOT, "runtime", "browser-choice.mjs"), path.join(source, "runtime", "browser-choice.mjs"));
await fs.writeFile(path.join(source, "bin", "gstack"), `#!/usr/bin/env node
import { main } from "../runtime/cli.js";
process.exitCode = await main(process.argv.slice(2));
`, { mode: 0o755 });
await fs.writeFile(path.join(source, "cap", "tool"), "#!/bin/sh\nprintf 'fixture capability %s\\n' \"$*\"\n", { mode: 0o755 });
await fs.writeFile(path.join(source, "browse", "dist", "browse"), `#!/usr/bin/env node
process.stdout.write(process.env.GSTACK_CHROMIUM_PATH || "unset");
`, { mode: 0o755 });
}
function fixtureCli(label: string) {
+227 -7
View File
@@ -3,10 +3,13 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { PassThrough, Readable } from "node:stream";
import { runDoctor } from "../runtime/doctor.js";
import { runInstallerCli, runtimeSlotVersion, runtimeSurfaceForCapabilities } from "../runtime/install.js";
import { resolveRuntimePaths } from "../runtime/paths.js";
import { setupRuntime } from "../runtime/setup.js";
import { configSetBrowserChoice } from "../runtime/config.js";
import { detectInstalledBrowsers, resolveBrowserChoice } from "../runtime/browser-choice.mjs";
import { bashCandidates, resolveBashCommand } from "../runtime/tooling.js";
import {
BOOTSTRAP_SCHEMA_VERSION,
@@ -96,13 +99,13 @@ describe("GStack runtime setup UX", () => {
}));
const retained = capture();
expect(await runInstallerCli([
"--source", source, "--home", home, "--capabilities", "pdf", "--dry-run", "--json",
"--source", source, "--home", home, "--capabilities", "pdf", "--browser", "managed", "--dry-run", "--json",
], { stdout: retained.stream, stderr: retained.stream })).toBe(0);
expect(JSON.parse(retained.value()).preview.capabilities).toEqual(["browser", "design", "diagram", "pdf"]);
const replaced = capture();
expect(await runInstallerCli([
"--source", source, "--home", home, "--capabilities", "pdf", "--replace-capabilities", "--dry-run", "--json",
"--source", source, "--home", home, "--capabilities", "pdf", "--browser", "managed", "--replace-capabilities", "--dry-run", "--json",
], { stdout: replaced.stream, stderr: replaced.stream })).toBe(0);
expect(JSON.parse(replaced.value()).preview.capabilities).toEqual(["browser", "diagram", "pdf"]);
} finally {
@@ -146,6 +149,7 @@ describe("GStack runtime setup UX", () => {
"--source", path.resolve(import.meta.dir, ".."),
"--home", home,
"--capabilities", "browser",
"--browser", "managed",
"--dry-run",
"--json",
], {
@@ -166,6 +170,183 @@ describe("GStack runtime setup UX", () => {
}
});
test("installed-browser preview skips every managed Chromium payload without persisting the choice", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-installed-preview-"));
const home = path.join(root, "home");
const output = capture();
try {
expect(await runInstallerCli([
"--source", path.resolve(import.meta.dir, ".."),
"--home", home,
"--capabilities", "browser",
"--browser", "installed",
"--browser-path", process.execPath,
"--dry-run",
"--json",
], { stdout: output.stream, stderr: output.stream })).toBe(0);
const preview = JSON.parse(output.value()).preview;
expect(preview.browser).toEqual({ provider: "installed", executablePath: await fs.realpath(process.execPath) });
expect(preview.materializations.some((item) => item.kind === "playwright-chromium-download")).toBe(false);
expect(runtimeSurfaceForCapabilities(["browser"], {
browserChoice: preview.browser,
}).entries.some((entry) => entry.path === ".gstack-runtime-browsers")).toBe(false);
await expect(fs.stat(home)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("installed-browser detection preserves wrapper paths, deduplicates physical targets, and rejects invalid files", async () => {
if (process.platform === "win32") return;
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-browser-detect-"));
try {
const physical = path.join(root, "snap");
const chrome = path.join(root, "google-chrome");
const chromium = path.join(root, "chromium");
const invalid = path.join(root, "not-executable");
await fs.writeFile(physical, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
await fs.writeFile(invalid, "not executable\n", { mode: 0o644 });
await fs.symlink(physical, chrome);
await fs.symlink(physical, chromium);
const detected = await detectInstalledBrowsers({
platform: "linux",
env: { PATH: root },
homeDir: root,
});
expect(detected).toEqual([{ name: "Google Chrome", executablePath: chrome }]);
expect(await resolveBrowserChoice({ provider: "installed", executablePath: chrome }, { platform: "linux" }))
.toEqual({ provider: "installed", executablePath: chrome });
await expect(resolveBrowserChoice({ provider: "installed", executablePath: invalid }, { platform: "linux" }))
.rejects.toMatchObject({ code: "BROWSER_PATH_INVALID" });
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("interactive browser choice covers managed, installed, later, and invalid selections without installing", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-browser-choice-"));
const source = path.join(root, "minimal-source");
const installedBrowser = path.join(root, "google-chrome");
await fs.mkdir(source);
await fs.writeFile(installedBrowser, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
try {
const cases = [
{ answer: "m", label: "managed isolated Chromium", code: 0 },
{ answer: "1", label: installedBrowser, code: 0 },
{ answer: "l", label: "No browser provider was selected", code: 0 },
{ answer: "9", label: "Invalid browser selection", code: 1 },
];
for (const [index, fixture] of cases.entries()) {
const home = path.join(root, `home-${index}`);
const output = new PassThrough();
const input = new PassThrough() as PassThrough & { isTTY: boolean };
input.isTTY = true;
let outputValue = "";
let answeredBrowser = false;
let answeredInstall = false;
output.on("data", (chunk) => {
outputValue += String(chunk);
if (!answeredBrowser && outputValue.includes("Select m, a browser number, or l")) {
answeredBrowser = true;
input.write(`${fixture.answer}\n`);
}
if (!answeredInstall && outputValue.includes("Install this optional local runtime now?")) {
answeredInstall = true;
input.end("later\n");
}
});
const code = await runInstallerCli([
"--source", source,
"--home", home,
"--capabilities", "browser",
], {
stdin: input,
stdout: output,
stderr: output,
platform: "linux",
env: { ...process.env, PATH: root },
homeDir: root,
});
expect(code, outputValue).toBe(fixture.code);
expect(outputValue).toContain(fixture.label);
await expect(fs.stat(home)).rejects.toMatchObject({ code: "ENOENT" });
}
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("browser bootstrap options are local-only and browser preview requires an explicit choice", async () => {
const output = capture();
let fetches = 0;
const browser = await fs.realpath(process.execPath);
expect(await bootstrapMain([
"options", "--capability", "browser", "--json",
], {
stdout: output.stream,
stderr: output.stream,
browserCandidates: [{ name: "Fixture Chromium", executablePath: browser }],
fetch: async () => { fetches += 1; throw new Error("unexpected fetch"); },
})).toBe(0);
expect(JSON.parse(output.value())).toMatchObject({
ok: true,
action: "options",
mutated: false,
network: false,
installed: [{ name: "Fixture Chromium", executablePath: browser }],
});
expect(fetches).toBe(0);
const missing = capture();
expect(await bootstrapMain(["preview", "--capability", "browser"], {
stdout: missing.stream,
stderr: missing.stream,
fetch: async () => { fetches += 1; throw new Error("unexpected fetch"); },
})).toBe(1);
expect(missing.value()).toContain("Choose a browser provider");
expect(fetches).toBe(0);
});
test("official installed-browser preview reports exact adapter bytes and omits browser binaries", async () => {
const output = capture();
const target = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
let fetches = 0;
expect(await bootstrapMain([
"preview", "--capability", "browser", "--browser", "installed",
"--browser-path", process.execPath, "--json",
], {
stdout: output.stream,
stderr: output.stream,
libc: process.platform === "linux" ? "glibc" : undefined,
fetch: async (url: string) => {
fetches += 1;
return { ok: true, url, json: async () => officialManifestFixture(target) };
},
})).toBe(0);
const result = JSON.parse(output.value());
expect(result.browser).toEqual({ provider: "installed", executablePath: await fs.realpath(process.execPath) });
expect(result.components).toEqual(["browser-code", "core"]);
expect(result.downloads.map((item) => item.component)).toEqual(["browser-code", "core"]);
expect(result.downloadBytes).toBe(16);
expect(fetches).toBe(1);
});
test("visible GStack Browser refuses installed Chrome before any network request", async () => {
const output = capture();
let fetches = 0;
expect(await bootstrapMain([
"preview", "--capability", "browser-visible", "--browser", "installed",
"--browser-path", process.execPath,
], {
stdout: output.stream,
stderr: output.stream,
fetch: async () => { fetches += 1; throw new Error("unexpected fetch"); },
})).toBe(1);
expect(output.value()).toContain("requires managed Chromium");
expect(fetches).toBe(0);
});
test("Windows Bash discovery shared by doctor and launchers finds a standard Git installation", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-git-bash-"));
const bash = path.join(root, "Git", "bin", "bash.exe");
@@ -239,6 +420,7 @@ describe("GStack runtime setup UX", () => {
await fs.writeFile(paths.versionPointer, JSON.stringify({
schemaVersion: 2, status: "active", current: "fixture", lastKnownGood: "fixture",
}));
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
const report = await runDoctor({ home, cwd: root, nodeCommand: process.execPath });
expect(report.ok).toBe(false);
expect(report.checks.find((check) => check.id === "capability:pdf")).toMatchObject({ status: "fail" });
@@ -274,6 +456,7 @@ describe("GStack runtime setup UX", () => {
await fs.writeFile(paths.versionPointer, JSON.stringify({
schemaVersion: 2, status: "active", current: "fixture", lastKnownGood: "fixture",
}));
await configSetBrowserChoice(home, { provider: "managed", executablePath: null });
const report = await runDoctor({ home, cwd: root, nodeCommand: process.execPath });
expect(report.checks.find((check) => check.id === "capability:browser")).toMatchObject({
status: "pass",
@@ -285,6 +468,43 @@ describe("GStack runtime setup UX", () => {
}
});
test("doctor launches the explicitly selected installed browser through the same Playwright adapter", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "gstack-doctor-installed-browser-"));
const home = path.join(root, "home");
try {
await setupRuntime({ home, cwd: root });
const paths = resolveRuntimePaths({ home });
const active = path.join(paths.versions, "fixture");
const managedBun = path.join(active, ".gstack-runtime-tools", process.platform === "win32" ? "bun.exe" : "bun");
const playwright = path.join(active, "node_modules", "playwright");
await fs.mkdir(path.dirname(managedBun), { recursive: true });
await fs.mkdir(playwright, { recursive: true });
await fs.copyFile(process.execPath, managedBun);
if (process.platform !== "win32") await fs.chmod(managedBun, 0o755);
const executable = await fs.realpath(process.execPath);
await fs.writeFile(path.join(playwright, "index.mjs"),
`export const chromium = { launch: async ({ headless, executablePath }) => { if (headless !== true || executablePath !== ${JSON.stringify(executable)}) throw new Error("wrong installed-browser launch"); return { version: () => "fixture-installed", close: async () => {} }; } };\n`);
await fs.writeFile(path.join(active, ".gstack-bundle.json"), JSON.stringify({
compatibility: { skillApi: "2.0" },
selectedCapabilities: ["browser"],
capabilities: { browse: "browse/dist/browse" },
tools: { bun: { path: path.relative(active, managedBun).split(path.sep).join("/"), version: "1.3.14" } },
}));
await fs.writeFile(paths.versionPointer, JSON.stringify({
schemaVersion: 2, status: "active", current: "fixture", lastKnownGood: "fixture",
}));
await configSetBrowserChoice(home, { provider: "installed", executablePath: executable });
const report = await runDoctor({ home, cwd: root, nodeCommand: process.execPath });
expect(report.checks.find((check) => check.id === "browser-selection")).toMatchObject({ status: "pass" });
expect(report.checks.find((check) => check.id === "capability:browser")).toMatchObject({
status: "pass",
details: { provider: "installed", executablePath: executable, version: "fixture-installed" },
});
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("bootstrap help has no dependency or network side effects", async () => {
const output = capture();
let fetches = 0;
@@ -303,7 +523,7 @@ describe("GStack runtime setup UX", () => {
let calls = 0;
try {
expect(await bootstrapMain([
"preview", "--capability", "browser-visible", "--home", path.join(root, "home"),
"preview", "--capability", "browser-visible", "--browser", "managed", "--home", path.join(root, "home"),
], {
stdout: output.stream,
stderr: output.stream,
@@ -355,7 +575,7 @@ describe("GStack runtime setup UX", () => {
arrayBuffer: async () => new TextEncoder().encode("tampered").buffer,
};
};
expect(await bootstrapMain(["install", "--capability", "browser", "--yes"], {
expect(await bootstrapMain(["install", "--capability", "browser", "--browser", "managed", "--yes"], {
stdout: output.stream,
stderr: output.stream,
fetch: fetch_,
@@ -381,7 +601,7 @@ describe("GStack runtime setup UX", () => {
test("official Linux bootstrap rejects musl explicitly before any network request", async () => {
const output = capture();
let fetches = 0;
expect(await bootstrapMain(["install", "--capability", "browser"], {
expect(await bootstrapMain(["install", "--capability", "browser", "--browser", "managed"], {
platform: "linux",
arch: "x64",
libc: "musl",
@@ -397,7 +617,7 @@ describe("GStack runtime setup UX", () => {
const output = capture();
const target = `${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`;
let calls = 0;
expect(await bootstrapMain(["install", "--capability", "browser", "--yes"], {
expect(await bootstrapMain(["install", "--capability", "browser", "--browser", "managed", "--yes"], {
stdout: output.stream,
stderr: output.stream,
fetch: async (url: string) => {
@@ -429,7 +649,7 @@ describe("GStack runtime setup UX", () => {
process.env.BOOTSTRAP_TEST_LOG = log;
try {
expect(await bootstrapMain([
"install", "--source", root, "--capability", "pdf", "--home", path.join(root, "home"), "--yes",
"install", "--source", root, "--capability", "pdf", "--browser", "managed", "--home", path.join(root, "home"), "--yes",
], { stdout: output.stream, stderr: output.stream })).toBe(0);
} finally {
if (previous == null) delete process.env.BOOTSTRAP_TEST_LOG;
+9 -6
View File
@@ -61,16 +61,19 @@ describe('GStack 2 canonical skill UX', () => {
for (const tree of TREE_NAMES) {
const runtime = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'RUNTIME.md'), 'utf8');
const bootstrap = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-bootstrap.mjs'));
const browserChoice = fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'browser-choice.mjs'));
const contract = JSON.parse(fs.readFileSync(path.join(ROOT, 'skills', tree, 'references', 'support', 'runtime-contract.json'), 'utf8'));
expect(bootstrap, tree).toEqual(source);
expect(browserChoice, tree).toEqual(fs.readFileSync(path.join(ROOT, 'runtime', 'browser-choice.mjs')));
expect(contract, tree).toEqual({ schemaVersion: 1, runtimeVersion: '2.0.0', skillApi: '2.0' });
expect(runtime, tree).toContain('preview --capability <name>');
expect(runtime, tree).toContain('It never downloads components or mutates runtime state.');
expect(runtime, tree).toContain('install --capability <name> --yes');
expect(runtime, tree).toContain('options --capability <name>');
expect(runtime, tree).toContain('gstack config browser clear');
expect(runtime, tree).toContain('Never run `./setup` inside a standard-installed skill directory');
expect(runtime, tree).toContain('Deferring installation records no consent');
expect(runtime, tree).toContain('Logical `browser` expands to `browser-code + browser-headless`');
expect(runtime, tree).toContain('`browser-visible` expands to `browser-code + browser-visible` and does not require headless');
expect(runtime, tree).toContain('With managed Chromium, logical `browser` expands to `browser-code + browser-headless`');
expect(runtime, tree).toContain('Internal `browser-visible` expands to `browser-code + browser-visible` and is managed-only');
expect(runtime, tree).toContain('`pdf` depends on `diagram`');
expect(runtime, tree).toContain('`all` means those five and intentionally excludes visible Chromium');
expect(runtime, tree).toContain('summed compressed bytes');
@@ -85,8 +88,8 @@ describe('GStack 2 canonical skill UX', () => {
for (const source of ['open-gstack-browser', 'pair-agent', 'setup-browser-cookies']) {
const body = fs.readFileSync(ownerModule(source), 'utf8');
expect(body, source).toContain('## Visible-browser point-of-use gate');
expect(body, source).toContain('preview --capability browser-visible');
expect(body, source).toContain('install --capability browser-visible --yes');
expect(body, source).toContain('preview --capability browser-visible --browser managed');
expect(body, source).toContain('install --capability browser-visible --browser managed --yes');
expect(body, source).toContain('never requires `browser-headless`');
}
expect(fs.readFileSync(ownerModule('browse'), 'utf8')).not.toContain('browser-visible');
@@ -124,7 +127,7 @@ describe('GStack 2 canonical skill UX', () => {
let stdout = '';
let stderr = '';
const code = await module.main([
'install', '--source', source, '--capability', 'browser', '--home', home, '--yes',
'install', '--source', source, '--capability', 'browser', '--browser', 'managed', '--home', home, '--yes',
], {
stdout: { write: (chunk: string) => { stdout += chunk; } },
stderr: { write: (chunk: string) => { stderr += chunk; } },
+7 -2
View File
@@ -34,6 +34,7 @@ describe("release and CI hardening", () => {
});
expect(pkg.files).toEqual(["bin/gstack", "runtime", "README.md", "LICENSE", "VERSION"]);
expect(pkg.dependencies["puppeteer-core"]).toBeUndefined();
expect(pkg.dependencies.playwright).toBe("npm:playwright-core@^1.58.2");
});
test("runtime identity is aligned independently of the legacy four-slot release counter", () => {
@@ -53,6 +54,7 @@ describe("release and CI hardening", () => {
expect(workflow).toContain("versions/current.json");
expect(workflow).not.toContain('active="$GSTACK_HOME/versions/2.0.0"');
expect(workflow).toContain(".gstack-runtime-browsers");
expect(workflow).toContain("--browser managed");
// Exercise both the bundled browser and the explicit Chromium channel. Keep
// this semantic: the workflow intentionally loops over launch options so a
// harmless refactor does not invalidate release hardening.
@@ -68,6 +70,8 @@ describe("release and CI hardening", () => {
expect(workflow).toContain("pathToFileURL(p).href");
expect(workflow).toContain('path").join(process.env.GITHUB_WORKSPACE,".gstack-runtime-smoke.html")');
expect(workflow).not.toContain("goto about:blank");
expect(read("scripts/gstack2/runtime-install-smoke.sh"))
.toContain('./setup --home "$HOME_DIR" --browser managed --install-now --yes --json');
const manifest = read(".github/scripts/create-runtime-release-manifest.mjs");
expect(manifest).toContain("bytes: stat.size");
expect(manifest).toContain('certificateOidcIssuer: "https://token.actions.githubusercontent.com"');
@@ -85,6 +89,7 @@ describe("release and CI hardening", () => {
const installer = read("runtime/install.js");
expect(installer).toContain('entry("runtime")');
expect(installer).toContain('entry(managedBunRelativePath(), "managed-bun", true)');
expect(installer).not.toContain('entry("node_modules/playwright-core")');
const browser = read("browse/src/cli.ts");
expect(browser).toContain("Every installed/compiled client must use the adjacent Node-compatible daemon");
expect(browser).toContain("export function resolveServerLaunchTarget(");
@@ -93,8 +98,8 @@ describe("release and CI hardening", () => {
test("Windows setup lane installs, doctors, and uninstalls rather than only building", () => {
const workflow = read(".github/workflows/windows-setup-e2e.yml");
expect(workflow).toContain("--dry-run --capabilities browser");
expect(workflow).toContain("--install-now --yes --capabilities browser");
expect(workflow).toContain("--dry-run --capabilities browser --browser managed");
expect(workflow).toContain("--install-now --yes --capabilities browser --browser managed");
expect(workflow).toContain("doctor --json");
expect(workflow).toContain("runtime/cli.js uninstall");
});