mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
fix(process): derive group settlement from completed native facts (#10043)
fix(process): settle native-confirmed absent group members
This commit is contained in:
1 parent
55c746afaf
commit
dd522bf9a7
2 files changed
+105
-19
No files matched your search
@@ -82,7 +82,6 @@ export const observeOwnedProcessGroups = async (
|
||||
host,
|
||||
signal,
|
||||
);
|
||||
const uncertainGroups = new Set<number>();
|
||||
const candidateProcesses = observationRows.filter((process) => {
|
||||
if (retainedPids.has(process.pid)) return true;
|
||||
const identity = firstIdentities.get(process.pid);
|
||||
@@ -93,15 +92,8 @@ export const observeOwnedProcessGroups = async (
|
||||
groupIds.includes(identity.relation.processGroupId)
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
groupIds.includes(process.processGroupId) &&
|
||||
identity.relation?.state !== "observed"
|
||||
)
|
||||
uncertainGroups.add(process.processGroupId);
|
||||
return groupIds.includes(process.processGroupId);
|
||||
}
|
||||
if (groupIds.includes(process.processGroupId))
|
||||
uncertainGroups.add(process.processGroupId);
|
||||
return groupIds.includes(process.processGroupId);
|
||||
});
|
||||
const facts = await readProcessOwnershipFacts(
|
||||
@@ -119,24 +111,26 @@ export const observeOwnedProcessGroups = async (
|
||||
scopeFacts: facts,
|
||||
});
|
||||
const observations = new Map<number, ProcessGroupObservation>(
|
||||
groupIds.map((groupId) => [
|
||||
groupId,
|
||||
uncertainGroups.has(groupId)
|
||||
? {
|
||||
state: "unverifiable",
|
||||
reason: "native process-group membership could not be verified",
|
||||
}
|
||||
: { state: "empty" },
|
||||
]),
|
||||
groupIds.map((groupId) => [groupId, { state: "empty" }]),
|
||||
);
|
||||
for (const fact of facts) {
|
||||
if (fact.identityAfter.state === "absent") continue;
|
||||
if (processIdentityHasExited(fact.identityAfter)) continue;
|
||||
// An initial unreadable membership remains uncertain only while the
|
||||
// final native fact does not establish that this candidate has exited.
|
||||
if (
|
||||
groupIds.includes(fact.process.processGroupId) &&
|
||||
(fact.identityBefore.state !== "readable" ||
|
||||
fact.identityBefore.relation?.state !== "observed")
|
||||
)
|
||||
observations.set(fact.process.processGroupId, {
|
||||
state: "unverifiable",
|
||||
reason: "native process-group membership could not be verified",
|
||||
});
|
||||
const nativeGroupId =
|
||||
fact.identityAfter.state === "readable" &&
|
||||
fact.identityAfter.relation?.state === "observed"
|
||||
? fact.identityAfter.relation.processGroupId
|
||||
: undefined;
|
||||
if (processIdentityHasExited(fact.identityAfter)) continue;
|
||||
const candidateGroups = groupIds.filter(
|
||||
(groupId) =>
|
||||
nativeGroupId === groupId ||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type {
|
||||
ProcessIdentityObservation,
|
||||
@@ -151,3 +152,94 @@ describe("capture ownership evidence lifetime", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"absent",
|
||||
"absent-after-unavailable",
|
||||
"zombie",
|
||||
"unavailable",
|
||||
] as const)(
|
||||
"classifies completed inert-child settlement with native %s facts",
|
||||
async (native) => {
|
||||
const child = spawn(process.execPath, ["-e", "process.exit(0)"], {
|
||||
detached: process.platform !== "win32",
|
||||
stdio: "ignore",
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) =>
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`Inert child exit ${String(code)}`)),
|
||||
);
|
||||
});
|
||||
if (child.pid === undefined) throw new Error("Missing inert child PID");
|
||||
// The child exited for real; the host supplies a stale table row and
|
||||
// controlled native facts. This qualifies settlement, not the native reader.
|
||||
const row = {
|
||||
pid: child.pid,
|
||||
parentPid: process.pid,
|
||||
processGroupId: child.pid,
|
||||
state: "S",
|
||||
command: "inert fixture",
|
||||
};
|
||||
const observation: ProcessIdentityObservation =
|
||||
native === "absent" || native === "absent-after-unavailable"
|
||||
? { state: "absent", reason: "not-in-snapshot" }
|
||||
: native === "zombie"
|
||||
? {
|
||||
state: "readable",
|
||||
identity: "fixture-start",
|
||||
lifecycle: { state: "observed", lifecycle: "zombie" },
|
||||
relation: {
|
||||
state: "observed",
|
||||
parentPid: process.pid,
|
||||
processGroupId: child.pid,
|
||||
},
|
||||
}
|
||||
: { state: "unavailable", reason: "native identity unavailable" };
|
||||
let identityReads = 0;
|
||||
const signalGroup = vi.fn();
|
||||
const host: ProcessOwnershipHost = {
|
||||
listProcesses: async () => [row],
|
||||
processIdentities: async (rows) => {
|
||||
identityReads += 1;
|
||||
return new Map(
|
||||
rows.map(({ pid }) => [
|
||||
pid,
|
||||
native === "absent-after-unavailable" && identityReads === 1
|
||||
? {
|
||||
state: "unavailable" as const,
|
||||
reason: "initial native read unavailable",
|
||||
}
|
||||
: observation,
|
||||
]),
|
||||
);
|
||||
},
|
||||
runTokens: async (rows) =>
|
||||
new Map(
|
||||
rows.map(({ pid }) => [
|
||||
pid,
|
||||
{
|
||||
state: "unavailable" as const,
|
||||
reason: "completed child has no token",
|
||||
},
|
||||
]),
|
||||
),
|
||||
environment: async () => ({}),
|
||||
signalGroup,
|
||||
};
|
||||
const settled = await observeSettlement({
|
||||
runId: "inert-run",
|
||||
processGroupIds: [child.pid],
|
||||
ownershipEvidence: new ProcessOwnershipEvidenceOwner(),
|
||||
settleMs: 100,
|
||||
platform: "darwin",
|
||||
ownershipHost: host,
|
||||
});
|
||||
expect(settled.state).toBe(
|
||||
native === "unavailable" ? "unverifiable" : "quiesced",
|
||||
);
|
||||
expect(signalGroup).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user