Files
rea/tests/boundary/providers/hopper/linuxPrivateDisplay.test.ts
morluto 60149282fc test: strengthen integration oracles and verification (#8295)
* test: strengthen integration oracles and verification

* test: bound lifecycle gates and strengthen workflow checks

* test: verify complete paged and extracted results

* test: preserve bytecode and cancellation evidence

* test: strengthen retained evidence and fixture cleanup

* test: require cross-projection bridge identities

* test: retain fixture owners through cleanup failures

* test: verify graph examples and project selectors

* test: tighten provider and lifecycle regressions

* test: consolidate hex coverage and bound cutter cleanup

* test: bound subprocess output waits and strengthen graph checks

* test: check mcp recovery and preserve cleanup owners

* test: verify structured mcp argument failures

* test: preserve artifact cleanup and gdb limit evidence

* test: preserve evidence identities and bound provider gates

* test: retain browser and comparison cleanup owners

* test: bound startup and strengthen fixture oracles

* test: bound lifecycle gates and verify recovered evidence

* test: verify public outcomes and lifecycle ownership

* test: assert precise evidence and release lifecycle gates

* test: verify selected facts and bound cleanup waits

* test: check retained evidence and decoded records

* test: verify final reports and retain cleanup ownership

* test: bound reviewed startup gates and retain cleanup

* test: verify PE errors and preserve MCP cleanup

* test: preserve MCP fixture cleanup ownership

* test: release provider gates before late startup
2026-11-01 00:15:28 +08:00

573 lines
19 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { describe, expect, it, onTestFinished, vi } from "vitest";
import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as ownership from "../../../../src/process/ProcessOwnership.js";
import {
DEFAULT_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
HOPPER_DEMO_DIALOG_TIMEOUT_SETTING,
MAXIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
MINIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
type HopperStartupDiagnostic,
type HopperStartupFailureCode,
} from "../../../../src/domain/hopperStartupFailure.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { HopperProcessError } from "../../../../src/domain/hopperErrors.js";
import { analysisErrorProjectionSchema } from "../../../../src/contracts/errorSchemas.js";
import {
runLinuxPrivateDisplayProbe,
selectLinuxPrivateDisplayStrategy,
type LinuxPrivateDisplayProbeProcessResult,
type LinuxPrivateDisplayProbeRunner,
type LinuxPrivateDisplayRunnableStrategy,
} from "../../../../src/hopper/LinuxPrivateDisplayProbe.js";
import {
LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX,
parseLinuxPrivateDisplayDiagnostic,
} from "../../../../src/hopper/LinuxPrivateDisplayDiagnostic.js";
const helperPath = fileURLToPath(
new URL("../../../../scripts/hopper-demo-x11.py", import.meta.url),
);
const hangingHelperPath = fileURLToPath(
new URL("../../../fixtures/x11ProbeHang.py", import.meta.url),
);
const dialogDetectionFixturePath = fileURLToPath(
new URL("../../../fixtures/hopperDialogDetection.py", import.meta.url),
);
const dialogOutcomeFixturePath = fileURLToPath(
new URL("../../../fixtures/hopperDemoDialogOutcome.py", import.meta.url),
);
const pythonCommand = ["python3", "python"].find(
(candidate) =>
spawnSync(candidate, ["--version"], { stdio: "ignore" }).status === 0,
);
const cleanupTimeoutFixturePath = fileURLToPath(
new URL("../../../fixtures/hopperCleanupTimeout.py", import.meta.url),
);
const ownerLifetimeFixturePath = fileURLToPath(
new URL("../../../fixtures/hopperOwnerLifetime.py", import.meta.url),
);
const displaySelectionFixturePath = fileURLToPath(
new URL("../../../fixtures/hopperDisplaySelection.py", import.meta.url),
);
type DiagnosticOverrides = Partial<
Omit<HopperStartupDiagnostic, "failure_code" | "status">
> &
(
| { readonly status?: "ready"; readonly failure_code?: null }
| {
readonly status: "error";
readonly failure_code: HopperStartupFailureCode;
}
);
const diagnostic = (overrides: DiagnosticOverrides = {}) => {
const common = {
component: "hopper_private_display",
operation: "probe",
reason: "ready",
socket_directory: "/tmp/.X11-unix",
socket_directory_mode: "1777",
mount_read_only: false,
effective_socket_directory_mode: "1777",
effective_mount_read_only: false,
wsl: false,
strategy: "direct",
fallback_reason: null,
xvfb_stderr_bytes: 0,
} as const;
return overrides.status === "error"
? ({ ...common, ...overrides } satisfies HopperStartupDiagnostic)
: ({
...common,
...overrides,
status: "ready",
failure_code: null,
} satisfies HopperStartupDiagnostic);
};
const processResult = (
value: HopperStartupDiagnostic,
overrides: Partial<LinuxPrivateDisplayProbeProcessResult> = {},
): LinuxPrivateDisplayProbeProcessResult => ({
outcome: "exited",
exitCode: value.status === "ready" ? 0 : failureExit(value.failure_code),
stderr: `${LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX}${JSON.stringify(value)}\n`,
stderrBytes: 0,
cleanup: { status: "released" },
...overrides,
});
const failureExit = (code: HopperStartupFailureCode): number =>
code === "x11_socket_directory_unusable"
? 80
: code === "runtime_dependency_unavailable"
? 79
: code === "demo_dialog_absent"
? 81
: 70;
describe("Hopper demo dialog detection", () => {
it.skipIf(process.platform !== "linux")(
"uses the Hopper transient-parent relation instead of exact dialog geometry",
() => {
const result = spawnSync(
"/usr/bin/python3",
[dialogDetectionFixturePath, helperPath],
{ encoding: "utf8" },
);
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
dialog: [10, 20, 100, 1200, 284],
click: [136, 362],
wrong_parent_rejected: true,
});
},
);
});
describe("Hopper demo dialog timeout outcomes", () => {
it.skipIf(pythonCommand === undefined)(
"distinguishes an unowned launcher from an admitted launcher without a dialog",
() => {
const result = spawnSync(
pythonCommand ?? "python3",
[dialogOutcomeFixturePath],
{ encoding: "utf8" },
);
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
not_owned: { code: 74, reason: "process_ownership_mismatch" },
admitted_without_window: {
code: 81,
reason: "demo_dialog_absent",
},
admitted_with_window: {
code: 76,
reason: "unsupported_demo_dialog",
},
default_timeout: { ms: 10_000 },
configured_timeout: { ms: 25_000 },
requested_timeout: { ms: 500 },
invalid_timeout: {
code: 82,
reason: "invalid_dialog_timeout",
dialog_timeout: {
setting: HOPPER_DEMO_DIALOG_TIMEOUT_SETTING,
value: "-5",
source: "setting",
minimum_ms: MINIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
maximum_ms: MAXIMUM_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
default_ms: DEFAULT_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
},
},
watcher_no_window: {
code: 81,
reason: "demo_dialog_absent",
elapsed_ms: 1_000,
},
watcher_hopper_window_without_dialog: {
code: 76,
reason: "unsupported_demo_dialog",
elapsed_ms: 1_000,
},
watcher_dialog_after_legacy_limit: {
dialog: 10,
elapsed_ms: DEFAULT_HOPPER_DEMO_DIALOG_TIMEOUT_MS + 100,
legacy_limit_ms: DEFAULT_HOPPER_DEMO_DIALOG_TIMEOUT_MS,
},
});
const timeout = JSON.parse(result.stdout).invalid_timeout.dialog_timeout;
const parsed = parseLinuxPrivateDisplayDiagnostic(
`${LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX}${JSON.stringify({
...diagnostic({
status: "error",
operation: "launch",
reason: "invalid_dialog_timeout",
failure_code: "invalid_dialog_timeout",
}),
dialog_timeout: timeout,
})}\n`,
);
expect(parsed.ok).toBe(true);
if (!parsed.ok || parsed.value.status !== "error")
throw new Error("Expected an error diagnostic for the invalid timeout");
const projected = projectAnalysisError(
new HopperProcessError(82, { diagnostic: parsed.value }),
);
expect(projected).toMatchObject({
code: "provider_unavailable",
details: {
failure_code: "invalid_dialog_timeout",
exit_code: 82,
diagnostics: { dialog_timeout: timeout },
},
});
expect(projected.details).not.toHaveProperty("launch_rejection");
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(
true,
);
},
);
});
describe("Hopper process cleanup", () => {
it.skipIf(process.platform !== "linux").each(["startup", "ready"])(
"reaps only its own children when the owner pipe closes during %s",
(phase) => {
const result = spawnSync(
"/usr/bin/python3",
[ownerLifetimeFixturePath, helperPath, phase],
{ encoding: "utf8", timeout: 10_000 },
);
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
phase,
children_cleaned: true,
sentinel_alive: true,
});
},
);
it.skipIf(process.platform !== "linux")(
"kills a child that ignores SIGTERM after the bounded timeout",
() => {
const result = spawnSync(
"/usr/bin/python3",
[cleanupTimeoutFixturePath, helperPath],
{ encoding: "utf8", timeout: 5_000 },
);
expect(result.status, result.stderr).toBe(0);
},
);
});
describe("Hopper demo explicit display number", () => {
it.skipIf(process.platform !== "linux")(
"honors an explicit display zero instead of choosing a random display",
() => {
const result = spawnSync(
"/usr/bin/python3",
[displaySelectionFixturePath, helperPath],
{ encoding: "utf8", timeout: 5_000 },
);
expect(result.status, result.stderr).toBe(0);
const explicit = (display: string) => ({
authorized: [`:${display}`],
checked: [`/tmp/.X11-unix/X${display}`, `/tmp/.X${display}-lock`],
random_calls: 0,
reason: "xauth_failed",
});
expect(JSON.parse(result.stdout)).toEqual({
explicit_zero: explicit("0"),
explicit_zero_random_occupied: explicit("0"),
explicit_zero_occupied: {
authorized: [],
checked: ["/tmp/.X11-unix/X0"],
random_calls: 0,
reason: "address_collision",
},
explicit_one: explicit("1"),
omitted: {
authorized: [":123"],
checked: ["/tmp/.X11-unix/X123", "/tmp/.X123-lock"],
random_calls: 1,
reason: "xauth_failed",
},
});
},
);
});
describe("Linux private display selection", () => {
it("prefers a successful direct Xvfb probe", async () => {
const calls: LinuxPrivateDisplayRunnableStrategy[] = [];
const runProbe: LinuxPrivateDisplayProbeRunner = (strategy) => {
calls.push(strategy);
return Promise.resolve(processResult(diagnostic({ strategy })));
};
await expect(
selectLinuxPrivateDisplayStrategy({ helperPath, runProbe }),
).resolves.toMatchObject({ ok: true, strategy: "direct" });
expect(calls).toEqual(["direct"]);
});
it("uses a private mount namespace only for the exact socket conflict", async () => {
const calls: LinuxPrivateDisplayRunnableStrategy[] = [];
const runProbe: LinuxPrivateDisplayProbeRunner = (strategy) => {
calls.push(strategy);
return Promise.resolve(
strategy === "direct"
? processResult(
diagnostic({
strategy,
status: "error",
failure_code: "x11_socket_directory_unusable",
reason: "socket_directory_read_only",
socket_directory_mode: "0777",
mount_read_only: true,
effective_socket_directory_mode: "0777",
effective_mount_read_only: true,
wsl: true,
}),
)
: processResult(diagnostic({ strategy, wsl: true })),
);
};
const selected = await selectLinuxPrivateDisplayStrategy({
helperPath,
runProbe,
});
expect(selected).toMatchObject({
ok: true,
strategy: "user-mount-namespace",
diagnostic: {
socket_directory_mode: "0777",
mount_read_only: true,
effective_socket_directory_mode: "1777",
effective_mount_read_only: false,
wsl: true,
},
});
expect(calls).toEqual(["direct", "user-mount-namespace"]);
});
it.each([
["missing_xvfb", "runtime_dependency_unavailable", 79],
["address_collision", "private_display_unavailable", 70],
] as const)(
"does not mask deterministic direct failure %s with a namespace probe",
async (reason, failureCode, exitCode) => {
const calls: LinuxPrivateDisplayRunnableStrategy[] = [];
const runProbe: LinuxPrivateDisplayProbeRunner = (strategy) => {
calls.push(strategy);
return Promise.resolve(
processResult(
diagnostic({
strategy,
status: "error",
failure_code: failureCode,
reason,
}),
{ exitCode },
),
);
};
await expect(
selectLinuxPrivateDisplayStrategy({ helperPath, runProbe }),
).resolves.toMatchObject({ ok: false, exitCode });
expect(calls).toEqual(["direct"]);
},
);
it("rejects malformed helper diagnostics", async () => {
const runProbe: LinuxPrivateDisplayProbeRunner = () =>
Promise.resolve({
...processResult(diagnostic()),
exitCode: 70,
stderr: `${LINUX_PRIVATE_DISPLAY_DIAGNOSTIC_PREFIX}not-json`,
});
await expect(
selectLinuxPrivateDisplayStrategy({ helperPath, runProbe }),
).resolves.toMatchObject({
ok: false,
diagnostic: { reason: "diagnostic_malformed" },
});
});
it("reports missing helper dependencies without exposing raw stderr", () => {
for (const [option] of [
["--xauth", "missing_xauth"],
["--xvfb", "missing_xvfb"],
] as const) {
const result = spawnSync(
"/usr/bin/python3",
[helperPath, "--probe", option, "/rea/missing-executable"],
{ encoding: "utf8" },
);
expect(result.status).toBe(79);
const parsed = parseLinuxPrivateDisplayDiagnostic(result.stderr);
expect(parsed).toMatchObject({
ok: true,
value: {
status: "error",
failure_code: "runtime_dependency_unavailable",
},
});
if (parsed.ok)
expect(parsed.value.reason).toMatch(/^missing_(xauth|xvfb)$/u);
expect(result.stderr).not.toContain("Traceback");
}
});
it("kills the owned probe group when its absolute deadline expires", async () => {
const result = await selectLinuxPrivateDisplayStrategy({
helperPath: hangingHelperPath,
timeoutMs: 75,
runProbe: runLinuxPrivateDisplayProbe,
});
expect(result).toMatchObject({
ok: false,
diagnostic: { reason: "probe_timeout" },
});
const processes = spawnSync("/bin/ps", ["-eo", "args="], {
encoding: "utf8",
}).stdout;
expect(processes).not.toContain(hangingHelperPath);
});
});
describe("private display probe output settlement", () => {
it.skipIf(process.platform === "win32")(
"retains inherited diagnostics written after the launcher exits",
async () => {
const result = await runLinuxPrivateDisplayProbe("direct", {
helperPath: fileURLToPath(
new URL("../../../fixtures/x11ProbeOutput.py", import.meta.url),
),
timeoutMs: 5_000,
});
expect(result).toEqual({
outcome: "exited",
exitCode: 0,
stderr: "firstlast",
stderrBytes: Buffer.byteLength("firstlast"),
cleanup: { status: "released" },
});
},
);
});
it("passes the private X authority cookie through xauth stdin", () => {
const fixture = fileURLToPath(
new URL("../../../fixtures/hopperXauthority.py", import.meta.url),
);
const process = spawnSync("python3", ["-I", "-S", fixture, helperPath], {
encoding: "utf8",
timeout: 5_000,
});
expect(process.status, process.stderr).toBe(0);
expect(JSON.parse(process.stdout)).toMatchObject({
arguments: ["fixture-xauth", "-q", "-f", expect.any(String), "source", "-"],
stdin: `add :123 . ${"0123456789abcdef".repeat(2)}\n`,
cookie_in_argv: false,
mode: 0o600,
});
});
const inertProbe = async (keepLeader = false) => {
const root = await mkdtemp(join(tmpdir(), "rea-private-display-lifecycle-"));
const marker = join(root, "descendant-survived");
const ready = join(root, "ready");
const helper = join(root, "probe.py");
const child = `setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'survived'), 800)`;
await writeFile(
helper,
[
"import subprocess, sys, time",
`subprocess.Popen([${JSON.stringify(process.execPath)}, '-e', ${JSON.stringify(child)}], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)`,
`open(${JSON.stringify(ready)}, 'w').write('ready')`,
"sys.stderr.write('authored probe diagnostic')",
"sys.stderr.flush()",
...(keepLeader ? ["time.sleep(2)"] : []),
].join("\n"),
);
onTestFinished(() => rm(root, { recursive: true, force: true }));
return { helper, marker, ready };
};
it.skipIf(process.platform === "win32")(
"stops silent owned descendants before releasing a normally exited probe",
async () => {
const { helper, marker } = await inertProbe();
const result = await runLinuxPrivateDisplayProbe("direct", {
helperPath: helper,
timeoutMs: 5000,
});
expect(result).toMatchObject({
outcome: "exited",
exitCode: 0,
stderr: "authored probe diagnostic",
cleanup: { status: "released" },
});
await new Promise((resolve) => setTimeout(resolve, 1000));
await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.skipIf(process.platform === "win32").each(["exited", "cancelled"] as const)(
"retains refused probe cleanup and retries its exact owner after %s",
async (outcome) => {
const { helper, ready } = await inertProbe(outcome === "cancelled");
const controller = new AbortController();
const scheduled = vi.spyOn(globalThis, "setTimeout");
const cleared = vi.spyOn(globalThis, "clearTimeout");
onTestFinished(() => {
scheduled.mockRestore();
cleared.mockRestore();
});
const original = ownership.cleanupOwnedProcessGroup;
const cleanup = vi
.spyOn(ownership, "cleanupOwnedProcessGroup")
.mockImplementation(async () => {
// Finalization cancellation must not replace a completed probe outcome.
if (outcome === "exited") controller.abort("cancelled during cleanup");
return {
cleaned: false,
reason: "controlled display probe cleanup refusal",
};
});
let pending:
| Extract<
LinuxPrivateDisplayProbeProcessResult["cleanup"],
{ status: "pending" }
>
| undefined;
onTestFinished(async () => {
cleanup.mockRestore();
await pending?.lease.release();
});
const operation = runLinuxPrivateDisplayProbe("direct", {
helperPath: helper,
timeoutMs: 5000,
signal: controller.signal,
});
if (outcome === "cancelled") {
await vi.waitFor(async () => expect(await access(ready)).toBeUndefined());
controller.abort("caller stopped");
}
const result = await operation;
expect(result.outcome).toBe(outcome);
// The probe has settled despite refused cleanup; no abandoned output wait
// should keep its original deadline alive until a later cleanup retry.
const deadlines = scheduled.mock.calls.flatMap((arguments_, index) =>
arguments_[1] === 5000 ? [scheduled.mock.results[index]!.value] : [],
);
expect(deadlines.length).toBeGreaterThan(0);
for (const timer of deadlines) expect(cleared).toHaveBeenCalledWith(timer);
expect(result.stderr).toBe("authored probe diagnostic");
expect(result.cleanup.status).toBe("pending");
if (result.cleanup.status !== "pending")
throw new Error("Expected retained probe owner");
pending = result.cleanup;
const retainedRunId = cleanup.mock.calls[0]?.[0].runId;
await expect(pending.lease.release()).rejects.toThrow(
"controlled display probe cleanup refusal",
);
expect(
cleanup.mock.calls.every(([owner]) => owner.runId === retainedRunId),
).toBe(true);
cleanup.mockImplementation(original);
await pending.lease.release();
expect(pending.lease.released).toBe(true);
},
);