mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
599a8ee11d
Retain unresolved process identity and launch evidence through retryable cleanup. Share capture path normalization and preserve failed filesystem observations. Bound child output and drain Windows pipes without one-chunk throttling. Reviewed fixes include #1628, #2632, #2644, #2784, #2928, #2931, #2965, #2992, and #3011. Validation: 92 focused source tests passed; four platform-specific skips. Real Windows and Hopper lifecycle coverage was not rerun.
158 lines
5.0 KiB
TypeScript
158 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type { ProcessOwnershipHost } from "./ProcessOwnership.js";
|
|
import {
|
|
observeProcessStartIdentity,
|
|
observeProcessStartIdentityExit,
|
|
signalProcessWithStartIdentity,
|
|
} from "./ProcessOwnershipObservation.js";
|
|
|
|
const host = (identity: string | undefined): ProcessOwnershipHost => ({
|
|
platform: "linux",
|
|
listProcesses: async () =>
|
|
identity === undefined
|
|
? []
|
|
: [
|
|
{
|
|
pid: 42,
|
|
parentPid: 1,
|
|
processGroupId: 42,
|
|
uid: 1000,
|
|
state: "S",
|
|
command: "/bin/fixture",
|
|
},
|
|
],
|
|
environment: async () => ({}),
|
|
processIdentities: async () =>
|
|
new Map([[42, { state: "readable", identity: identity ?? "" }]]),
|
|
signalGroup: () => undefined,
|
|
});
|
|
|
|
describe("process start identity lease", () => {
|
|
it("captures a readable identity for a live PID", async () => {
|
|
await expect(
|
|
observeProcessStartIdentity(42, host("start-1")),
|
|
).resolves.toEqual({
|
|
state: "readable",
|
|
identity: "start-1",
|
|
});
|
|
});
|
|
|
|
it("confirms exit or PID replacement without treating unreadable state as exit", async () => {
|
|
await expect(
|
|
observeProcessStartIdentityExit(42, "start-1", host(undefined)),
|
|
).resolves.toBe("exited");
|
|
await expect(
|
|
observeProcessStartIdentityExit(42, "start-1", host("replacement-start")),
|
|
).resolves.toBe("exited");
|
|
await expect(
|
|
observeProcessStartIdentityExit(42, "start-1", host("start-1")),
|
|
).resolves.toBe("present");
|
|
const unavailable: ProcessOwnershipHost = {
|
|
...host("start-1"),
|
|
processIdentities: async () =>
|
|
new Map([[42, { state: "unavailable", reason: "reader unavailable" }]]),
|
|
};
|
|
await expect(
|
|
observeProcessStartIdentityExit(42, "start-1", unavailable),
|
|
).resolves.toBe("unverified");
|
|
});
|
|
|
|
it("does not adopt a replacement PID's first identity for an owned launch", async () => {
|
|
const replacement: ProcessOwnershipHost = {
|
|
...host("replacement-start"),
|
|
environment: async () => ({ REA_PROCESS_RUN_ID: "another-run" }),
|
|
};
|
|
await expect(
|
|
observeProcessStartIdentity(42, replacement, "captured-run"),
|
|
).resolves.toMatchObject({
|
|
state: "unavailable",
|
|
reason: expect.stringContaining("ownership"),
|
|
});
|
|
});
|
|
|
|
it("does not establish an owned launch when its token is unreadable", async () => {
|
|
const unreadable: ProcessOwnershipHost = {
|
|
...host("start-1"),
|
|
runTokens: async () =>
|
|
new Map([
|
|
[42, { state: "unavailable", reason: "environment_errno_EACCES" }],
|
|
]),
|
|
};
|
|
await expect(
|
|
observeProcessStartIdentity(42, unreadable, "captured-run"),
|
|
).resolves.toMatchObject({
|
|
state: "unavailable",
|
|
reason: expect.stringContaining("environment_errno_EACCES"),
|
|
});
|
|
});
|
|
|
|
it("rejects an identity that changes while the captured run token is read", async () => {
|
|
let identity = "start-1";
|
|
const changing: ProcessOwnershipHost = {
|
|
...host("start-1"),
|
|
processIdentities: async () =>
|
|
new Map([[42, { state: "readable", identity }]]),
|
|
environment: async () => {
|
|
identity = "replacement-start";
|
|
return { REA_PROCESS_RUN_ID: "captured-run" };
|
|
},
|
|
};
|
|
await expect(
|
|
observeProcessStartIdentity(42, changing, "captured-run"),
|
|
).resolves.toMatchObject({
|
|
state: "unavailable",
|
|
reason: expect.stringContaining("identity changed"),
|
|
});
|
|
});
|
|
|
|
it("signals the PID when its launch identity still matches", async () => {
|
|
const sendSignal = vi.fn();
|
|
await expect(
|
|
signalProcessWithStartIdentity(42, "start-1", "SIGKILL", {
|
|
host: host("start-1"),
|
|
sendSignal,
|
|
}),
|
|
).resolves.toBe("signaled");
|
|
expect(sendSignal).toHaveBeenCalledWith(42, "SIGKILL");
|
|
});
|
|
|
|
it("does not signal a reused PID even when its command is unchanged", async () => {
|
|
const sendSignal = vi.fn();
|
|
await expect(
|
|
signalProcessWithStartIdentity(42, "start-1", "SIGKILL", {
|
|
host: host("start-2"),
|
|
sendSignal,
|
|
}),
|
|
).resolves.toBe("identity-changed");
|
|
expect(sendSignal).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not signal when the PID identity is unavailable", async () => {
|
|
const unavailable: ProcessOwnershipHost = {
|
|
...host("start-1"),
|
|
processIdentities: async () =>
|
|
new Map([[42, { state: "unavailable", reason: "reader unavailable" }]]),
|
|
};
|
|
const sendSignal = vi.fn();
|
|
await expect(
|
|
signalProcessWithStartIdentity(42, "start-1", "SIGKILL", {
|
|
host: unavailable,
|
|
sendSignal,
|
|
}),
|
|
).resolves.toBe("unverified");
|
|
expect(sendSignal).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports an already-gone PID without signaling", async () => {
|
|
const sendSignal = vi.fn();
|
|
await expect(
|
|
signalProcessWithStartIdentity(42, "start-1", "SIGKILL", {
|
|
host: host(undefined),
|
|
sendSignal,
|
|
}),
|
|
).resolves.toBe("gone");
|
|
expect(sendSignal).not.toHaveBeenCalled();
|
|
});
|
|
});
|