mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* fix(process): preserve producer-owned inspection cancellation Prototype the explicit inspection origin and retained file/batch owner protocol. Downstream adapter migration and lease binding remain required before integration. * fix(process): type inspection ownership handoffs precisely * fix(process): retain plain inspection preparation cancellation * fix(process): retain explicit inspection cleanup and cancellation facts * fix(process): require explicit inspection facts and cleanup scope * refactor(process): remove redundant cleanup forwarding wrapper * fix(firmware): retain inspection cancellation and cleanup ownership * fix(wakaru): retain typed inspection outcomes and caller scope * fix(ghidra): preserve inspection facts and launch cleanup scope * fix(providers): preserve typed process inspection outcomes * refactor(process): remove obsolete inspection cancellation re-export * fix(native): preserve inspection preparation cleanup and cancellation * fix(registration): retain typed inspection cleanup during Git discovery * fix(process): preserve inspection deadline origin in adapter handoff * fix(process): consume the canonical inspection selector in binding * fix(native): preserve inspection deadlines and cleanup retry completion * test(process): narrow the retained Darwin preparation owner * fix(providers): preserve preparation ownership and timeout policy Narrow preparation facts through their canonical owner, preserve actual cleanup capabilities and recorded cancellation, and keep enclosing command deadlines authoritative. Retain raw inspection records through Java refusal and configuration projections without changing availability fallback policy. * refactor(process): use one inspection failure type owner * fix(providers): retain recorded secondary failures when rebinding * fix(ghidra): preserve normalized inspection facts through public projection * fix(process): normalize native process listing cancellation * docs(development): clarify failure provenance and cleanup verification * fix(process): preserve direct inspection cancellation facts * fix(process): retain inspection owners through termination * fix(process): preserve command inspection cleanup provenance * fix(desktop): preserve inspection facts after cleanup retry * refactor(process)!: migrate desktop capture with inspection producers * fix(desktop)!: preserve capture failure origins and cleanup owners (#10115) * fix(desktop): preserve capture failure origins and cleanup * fix(desktop): type keyring cleanup cancellation * fix(kuna)!: retain copy cleanup before retiring workspaces (#10114) * fix(kuna): own copy cancellation and descriptor cleanup * fix(kuna): close copy descriptors before workspace removal * test(kuna): type copy lifecycle gates for project library * docs(kuna): explain retained copy-file cleanup and staging ownership * docs(kuna): separate cache publication, integrity and cleanup guidance * fix(desktop): retain sampling inspection owners before release * fix(process): preserve capture file read cancellation origin * fix(process): retain capture inspection cancellation and cleanup owners * fix(process): retain sampling inspection failures in capture scope * fix(process): retain inspection failures through mandatory cleanup * fix(process): distinguish sampler shutdown from capture cancellation * fix(browser): retain startup EOF before later cancellation * fix(process): bind capture inspection cleanup to the owning run * test(process): migrate cancellation fixtures to typed owners Record explicit preparation and command cancellation facts, retain exact opaque reasons and physical causes, and guard the mocked version probe from native preparation. Keep the live Wakaru command control while adding a recording equivalent for bounded source verification. * fix(process): own sampling reads and settle inspection peers * fix(errors): retain authored recovery through cleanup composition * fix(process): retain all sampling inspection leaves for final cleanup * test(browser): report recording cleanup signal outcome * fix(process): keep inspection error context out of implicit JSON * fix(desktop): retain normalized sampling cleanup ownership * test(process): assert tagged startup cancellation
771 lines
22 KiB
TypeScript
771 lines
22 KiB
TypeScript
import { expect, it, vi } from "vitest";
|
|
|
|
import type {
|
|
ProcessIdentityObservation,
|
|
ProcessOwnershipFact,
|
|
ProcessOwnershipHost,
|
|
ProcessTableEntry,
|
|
ProcessRunTokenObservation,
|
|
ProcessNativeRelationObservation,
|
|
} from "./ProcessOwnership.js";
|
|
import { readProcessOwnershipFacts } from "./ProcessOwnershipObservation.js";
|
|
import { ProcessOwnershipEvidenceOwner } from "./ProcessOwnershipEvidence.js";
|
|
|
|
const row: ProcessTableEntry = {
|
|
pid: 42,
|
|
parentPid: 1,
|
|
processGroupId: 42,
|
|
state: "S",
|
|
command: "fixture",
|
|
};
|
|
|
|
const identity = (
|
|
value: string,
|
|
lifecycle: "non-zombie" | "zombie" | "dead" = "non-zombie",
|
|
relation: ProcessNativeRelationObservation = {
|
|
state: "observed",
|
|
parentPid: row.parentPid,
|
|
processGroupId: row.processGroupId,
|
|
},
|
|
): ProcessIdentityObservation => ({
|
|
state: "readable",
|
|
identity: value,
|
|
lifecycle: { state: "observed", lifecycle },
|
|
relation,
|
|
});
|
|
|
|
const hostFor = (
|
|
identities: readonly ReadonlyMap<number, ProcessIdentityObservation>[],
|
|
token: ProcessRunTokenObservation = { state: "readable", runId: "run" },
|
|
): ProcessOwnershipHost => {
|
|
let identityRead = 0;
|
|
return {
|
|
platform: "linux",
|
|
listProcesses: vi.fn(async () => {
|
|
throw new Error(
|
|
"ownership fact owner must not enumerate the process table",
|
|
);
|
|
}),
|
|
environment: async () => ({}),
|
|
processIdentities: async () => {
|
|
const result = identities[identityRead];
|
|
identityRead += 1;
|
|
return result ?? new Map();
|
|
},
|
|
runTokens: async () => new Map([[row.pid, token]]),
|
|
signalGroup: () => undefined,
|
|
};
|
|
};
|
|
|
|
it("binds owned, missing, and foreign tokens only to the same identity generation", async () => {
|
|
for (const token of [
|
|
{ state: "readable", runId: "run" },
|
|
{ state: "readable", runId: undefined },
|
|
{ state: "readable", runId: "foreign" },
|
|
] as const) {
|
|
const host = hostFor(
|
|
[
|
|
new Map([[row.pid, identity("start:42")]]),
|
|
new Map([[row.pid, identity("start:42")]]),
|
|
],
|
|
token,
|
|
);
|
|
const [fact] = await readProcessOwnershipFacts([row], host);
|
|
expect(fact).toMatchObject({
|
|
state: "same-generation",
|
|
token,
|
|
identityBefore: { state: "readable", identity: "start:42" },
|
|
identityAfter: { state: "readable", identity: "start:42" },
|
|
});
|
|
}
|
|
});
|
|
|
|
it("retains a token-matching replacement without authority until same-generation validation", async () => {
|
|
const host = hostFor([
|
|
new Map([[row.pid, identity("start:old")]]),
|
|
new Map([[row.pid, identity("start:new")]]),
|
|
]);
|
|
const facts = await readProcessOwnershipFacts([row], host);
|
|
expect(facts[0]).toMatchObject({
|
|
state: "replaced",
|
|
token: { state: "readable", runId: "run" },
|
|
});
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: row.pid,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
expect(owner.reconcile(facts, options)).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: undefined,
|
|
applicability: "identity-unresolved",
|
|
scopeEvidence: "native-identity-unresolved",
|
|
relation: undefined,
|
|
},
|
|
]);
|
|
expect(owner.coverage).toMatchObject({
|
|
retainedOwned: 0,
|
|
retainedUnresolved: 1,
|
|
uncertainPids: [row.pid],
|
|
});
|
|
expect(owner.rowsForObservation([])).toEqual([row]);
|
|
const stable = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor([
|
|
new Map([[row.pid, identity("start:new")]]),
|
|
new Map([[row.pid, identity("start:new")]]),
|
|
]),
|
|
);
|
|
expect(owner.reconcile(stable, options)).toMatchObject([
|
|
{ pid: row.pid, identity: "start:new", applicability: "token-verified" },
|
|
]);
|
|
});
|
|
|
|
it.each(["token-verified", "identity-unresolved"] as const)(
|
|
"retains a token-matching replacement after retiring a %s candidate",
|
|
async (applicability) => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: row.pid,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
const previousIdentity: ProcessIdentityObservation =
|
|
applicability === "token-verified"
|
|
? identity("start:old")
|
|
: { state: "unavailable", reason: "native identity denied" };
|
|
const previous = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor([
|
|
new Map([[row.pid, previousIdentity]]),
|
|
new Map([[row.pid, previousIdentity]]),
|
|
]),
|
|
);
|
|
expect(owner.reconcile(previous, options)).toMatchObject([
|
|
{ pid: row.pid, applicability },
|
|
]);
|
|
|
|
const replacement = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor([
|
|
new Map([[row.pid, identity("start:old")]]),
|
|
new Map([[row.pid, identity("start:new")]]),
|
|
]),
|
|
);
|
|
expect(owner.reconcile(replacement, options)).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: undefined,
|
|
applicability: "identity-unresolved",
|
|
relation: undefined,
|
|
},
|
|
]);
|
|
expect(owner.rowsForObservation([])).toEqual([row]);
|
|
expect(owner.coverage).toMatchObject({
|
|
retainedOwned: 0,
|
|
retainedUnresolved: 1,
|
|
uncertainPids: [row.pid],
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["token-verified", "identity-unresolved"] as const)(
|
|
"retires a %s candidate when its replacement has a foreign token",
|
|
async (applicability) => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: row.pid,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
const previousIdentity: ProcessIdentityObservation =
|
|
applicability === "token-verified"
|
|
? identity("start:old")
|
|
: { state: "unavailable", reason: "native identity denied" };
|
|
const previous = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor([
|
|
new Map([[row.pid, previousIdentity]]),
|
|
new Map([[row.pid, previousIdentity]]),
|
|
]),
|
|
);
|
|
expect(owner.reconcile(previous, options)).toMatchObject([
|
|
{ pid: row.pid, applicability },
|
|
]);
|
|
const replacement = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor(
|
|
[
|
|
new Map([[row.pid, identity("start:old")]]),
|
|
new Map([[row.pid, identity("start:foreign")]]),
|
|
],
|
|
{ state: "readable", runId: "foreign" },
|
|
),
|
|
);
|
|
|
|
expect(owner.reconcile(replacement, options)).toEqual([]);
|
|
expect(owner.rowsForObservation([])).toEqual([]);
|
|
expect(
|
|
new ProcessOwnershipEvidenceOwner().reconcile(replacement, options),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it.each(["absent", "zombie", "dead"] as const)(
|
|
"settles a token-matching replacement when its final native state is %s",
|
|
async (state) => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: row.pid,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
const previous = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor([
|
|
new Map([[row.pid, identity("start:old")]]),
|
|
new Map([[row.pid, identity("start:old")]]),
|
|
]),
|
|
);
|
|
owner.reconcile(previous, options);
|
|
const terminalIdentity: ProcessIdentityObservation =
|
|
state === "absent"
|
|
? { state: "absent", reason: "not-in-snapshot" }
|
|
: identity("start:new", state);
|
|
const replacement = await readProcessOwnershipFacts(
|
|
[row],
|
|
hostFor([
|
|
new Map([[row.pid, identity("start:old")]]),
|
|
new Map([[row.pid, terminalIdentity]]),
|
|
]),
|
|
);
|
|
|
|
expect(owner.reconcile(replacement, options)).toEqual([]);
|
|
expect(owner.rowsForObservation([])).toEqual([]);
|
|
expect(
|
|
new ProcessOwnershipEvidenceOwner().reconcile(replacement, options),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it("keeps omitted identities uncertain and native absence distinct", async () => {
|
|
const omitted = hostFor([new Map(), new Map()]);
|
|
const [uncertain] = await readProcessOwnershipFacts([row], omitted);
|
|
expect(uncertain).toMatchObject({
|
|
state: "unavailable",
|
|
identityBefore: {
|
|
state: "unavailable",
|
|
reason: "process identity was not returned during before observation",
|
|
},
|
|
});
|
|
|
|
const absent: ProcessIdentityObservation = {
|
|
state: "absent",
|
|
reason: "not-in-snapshot",
|
|
};
|
|
const absentHost = hostFor([
|
|
new Map([[row.pid, absent]]),
|
|
new Map([[row.pid, absent]]),
|
|
]);
|
|
const [gone] = await readProcessOwnershipFacts([row], absentHost);
|
|
expect(gone).toMatchObject({
|
|
state: "absent",
|
|
token: { state: "readable", runId: "run" },
|
|
});
|
|
expect(absentHost.listProcesses).not.toHaveBeenCalled();
|
|
|
|
const appeared = hostFor([
|
|
new Map([[row.pid, absent]]),
|
|
new Map([[row.pid, identity("start:appeared")]]),
|
|
]);
|
|
const [replacement] = await readProcessOwnershipFacts([row], appeared);
|
|
expect(replacement?.state).toBe("replaced");
|
|
});
|
|
|
|
it("keeps zombie lifecycle separate from identity and token diagnostics intact", async () => {
|
|
const zombie = hostFor(
|
|
[
|
|
new Map([[row.pid, identity("start:zombie", "zombie")]]),
|
|
new Map([[row.pid, identity("start:zombie", "zombie")]]),
|
|
],
|
|
{ state: "unavailable", reason: "environment denied" },
|
|
);
|
|
const [fact] = await readProcessOwnershipFacts([row], zombie);
|
|
expect(fact).toMatchObject({
|
|
state: "same-generation",
|
|
token: { state: "unavailable", reason: "environment denied" },
|
|
});
|
|
expect(fact?.identityBefore).toEqual(
|
|
expect.objectContaining({
|
|
identity: "start:zombie",
|
|
lifecycle: { state: "observed", lifecycle: "zombie" },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps stale table ancestry separate from current native membership", async () => {
|
|
const staleRow: ProcessTableEntry = {
|
|
...row,
|
|
pid: 101,
|
|
parentPid: 100,
|
|
processGroupId: 100,
|
|
};
|
|
const newGenerationRelation = {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 102,
|
|
} as const;
|
|
const host = hostFor([
|
|
new Map([
|
|
[
|
|
staleRow.pid,
|
|
identity("start:new", "non-zombie", newGenerationRelation),
|
|
],
|
|
]),
|
|
new Map([
|
|
[
|
|
staleRow.pid,
|
|
identity("start:new", "non-zombie", newGenerationRelation),
|
|
],
|
|
]),
|
|
]);
|
|
|
|
const [fact] = await readProcessOwnershipFacts([staleRow], host);
|
|
|
|
expect(fact).toMatchObject({
|
|
state: "same-generation",
|
|
process: { pid: 101, parentPid: 100, processGroupId: 100 },
|
|
identityBefore: { relation: newGenerationRelation },
|
|
identityAfter: { relation: newGenerationRelation },
|
|
});
|
|
expect(fact?.identityAfter).toMatchObject({
|
|
relation: { parentPid: 1, processGroupId: 102 },
|
|
});
|
|
});
|
|
|
|
it("retains both native relations when group membership changes during the read", async () => {
|
|
const staleRow: ProcessTableEntry = {
|
|
...row,
|
|
pid: 101,
|
|
parentPid: 100,
|
|
processGroupId: 100,
|
|
};
|
|
const beforeRelation = {
|
|
state: "observed",
|
|
parentPid: 100,
|
|
processGroupId: 100,
|
|
} as const;
|
|
const afterRelation = {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 102,
|
|
} as const;
|
|
const host = hostFor([
|
|
new Map([
|
|
[staleRow.pid, identity("start:same", "non-zombie", beforeRelation)],
|
|
]),
|
|
new Map([
|
|
[staleRow.pid, identity("start:same", "non-zombie", afterRelation)],
|
|
]),
|
|
]);
|
|
|
|
const [fact] = await readProcessOwnershipFacts([staleRow], host);
|
|
|
|
expect(fact?.state).toBe("same-generation");
|
|
expect(fact?.process).toEqual(staleRow);
|
|
expect(fact?.identityBefore).toMatchObject({ relation: beforeRelation });
|
|
expect(fact?.identityAfter).toMatchObject({ relation: afterRelation });
|
|
});
|
|
|
|
it("retains owned generations across retries after their token becomes unreadable", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const owned: ProcessOwnershipFact = {
|
|
process: row,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:owned"),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity("start:owned"),
|
|
};
|
|
owner.reconcile([owned], {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: 1,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
});
|
|
|
|
const later = {
|
|
...owned,
|
|
token: { state: "unavailable", reason: "environment denied" },
|
|
identityBefore: identity("start:owned"),
|
|
identityAfter: identity("start:owned", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 55,
|
|
}),
|
|
} satisfies ProcessOwnershipFact;
|
|
const retained = owner.reconcile([later], {
|
|
runId: "run",
|
|
listedRows: 0,
|
|
listedPids: new Set(),
|
|
leaderPid: 1,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
});
|
|
|
|
expect(retained).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: "start:owned",
|
|
applicability: "token-verified",
|
|
relation: { parentPid: 1, processGroupId: 55 },
|
|
},
|
|
]);
|
|
expect(owner.coverage).toMatchObject({
|
|
listedRows: 0,
|
|
retainedOnlyPids: 1,
|
|
retainedOwned: 1,
|
|
processTableCoverage: "unreported-by-host",
|
|
});
|
|
});
|
|
|
|
it("upgrades an identity-unresolved candidate only after a same-generation token match", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: 42,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
const unavailable: ProcessIdentityObservation = {
|
|
state: "unavailable",
|
|
reason: "native identity denied",
|
|
};
|
|
const first: ProcessOwnershipFact = {
|
|
process: row,
|
|
state: "unavailable",
|
|
identityBefore: unavailable,
|
|
token: { state: "unavailable", reason: "EPERM" },
|
|
identityAfter: unavailable,
|
|
};
|
|
expect(owner.reconcile([first], options)).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: undefined,
|
|
applicability: "identity-unresolved",
|
|
},
|
|
]);
|
|
|
|
const current: ProcessOwnershipFact = {
|
|
process: row,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:current"),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity("start:current"),
|
|
};
|
|
expect(owner.reconcile([current], options)).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: "start:current",
|
|
applicability: "token-verified",
|
|
scopeEvidence: "run-token",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("lets a successful native absence snapshot retire a token-matching old generation", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const owned: ProcessOwnershipFact = {
|
|
process: row,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:owned"),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity("start:owned"),
|
|
};
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: 1,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
owner.reconcile([owned], options);
|
|
|
|
const snapshotAbsence: ProcessIdentityObservation = {
|
|
state: "absent",
|
|
reason: "not-in-snapshot",
|
|
};
|
|
const contradictory = {
|
|
...owned,
|
|
state: "absent" as const,
|
|
identityBefore: snapshotAbsence,
|
|
identityAfter: snapshotAbsence,
|
|
} satisfies ProcessOwnershipFact;
|
|
|
|
expect(owner.reconcile([contradictory], options)).toEqual([]);
|
|
expect(owner.coverage.retainedOwned).toBe(0);
|
|
});
|
|
|
|
it("retains a newly verified replacement generation after retiring the old one", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([row.pid]),
|
|
leaderPid: 1,
|
|
processGroupIds: new Set([row.processGroupId]),
|
|
};
|
|
owner.reconcile(
|
|
[
|
|
{
|
|
process: row,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:old"),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity("start:old"),
|
|
},
|
|
],
|
|
options,
|
|
);
|
|
|
|
const replacement: ProcessOwnershipFact = {
|
|
process: row,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:new"),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity("start:new"),
|
|
};
|
|
expect(owner.reconcile([replacement], options)).toMatchObject([
|
|
{
|
|
pid: row.pid,
|
|
identity: "start:new",
|
|
applicability: "token-verified",
|
|
scopeEvidence: "run-token",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("retains only related unresolved rows and retires them on replacement", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const candidate: ProcessTableEntry = {
|
|
...row,
|
|
pid: 77,
|
|
parentPid: 42,
|
|
processGroupId: 55,
|
|
};
|
|
const unreadable: ProcessOwnershipFact = {
|
|
process: candidate,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:maybe-owned", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 42,
|
|
processGroupId: 55,
|
|
}),
|
|
token: { state: "unavailable", reason: "permission denied" },
|
|
identityAfter: identity("start:maybe-owned", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 42,
|
|
processGroupId: 55,
|
|
}),
|
|
};
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([77]),
|
|
leaderPid: 42,
|
|
processGroupIds: new Set<number>([42]),
|
|
};
|
|
expect(owner.reconcile([unreadable], options)).toMatchObject([
|
|
{
|
|
pid: 77,
|
|
applicability: "related-ownership-unresolved",
|
|
scopeEvidence: "capture-leader-pid",
|
|
},
|
|
]);
|
|
|
|
const reparented = {
|
|
...unreadable,
|
|
process: { ...candidate, parentPid: 1, processGroupId: 100 },
|
|
identityBefore: identity("start:maybe-owned", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 100,
|
|
}),
|
|
identityAfter: identity("start:maybe-owned", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 100,
|
|
}),
|
|
} satisfies ProcessOwnershipFact;
|
|
expect(
|
|
owner.reconcile([reparented], {
|
|
...options,
|
|
listedRows: 0,
|
|
listedPids: new Set(),
|
|
}),
|
|
).toMatchObject([
|
|
{
|
|
pid: 77,
|
|
applicability: "related-ownership-unresolved",
|
|
scopeEvidence: "previously-related-generation",
|
|
relation: { processGroupId: 100 },
|
|
},
|
|
]);
|
|
|
|
const replacement = {
|
|
...reparented,
|
|
state: "replaced" as const,
|
|
identityAfter: identity("start:replacement"),
|
|
};
|
|
expect(owner.reconcile([replacement], options)).toEqual([]);
|
|
});
|
|
|
|
it("requires a retained owned parent to be present as the same native generation", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const parent: ProcessTableEntry = {
|
|
...row,
|
|
pid: 70,
|
|
parentPid: 1,
|
|
processGroupId: 70,
|
|
};
|
|
const child: ProcessTableEntry = {
|
|
...row,
|
|
pid: 71,
|
|
parentPid: 70,
|
|
processGroupId: 71,
|
|
};
|
|
const parentFact: ProcessOwnershipFact = {
|
|
process: parent,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:parent", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 70,
|
|
}),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity("start:parent", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 70,
|
|
}),
|
|
};
|
|
const options = {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([parent.pid]),
|
|
leaderPid: 42,
|
|
processGroupIds: new Set<number>([42]),
|
|
};
|
|
owner.reconcile([parentFact], options);
|
|
|
|
const unreadableChild: ProcessOwnershipFact = {
|
|
process: child,
|
|
state: "same-generation",
|
|
identityBefore: identity("start:child", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 70,
|
|
processGroupId: 71,
|
|
}),
|
|
token: { state: "unavailable", reason: "permission denied" },
|
|
identityAfter: identity("start:child", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: parent.pid,
|
|
processGroupId: child.processGroupId,
|
|
}),
|
|
};
|
|
expect(
|
|
owner.reconcile([unreadableChild], {
|
|
...options,
|
|
listedRows: 1,
|
|
listedPids: new Set([child.pid]),
|
|
}),
|
|
).toMatchObject([{ pid: parent.pid, applicability: "token-verified" }]);
|
|
|
|
const currentParent = {
|
|
...parentFact,
|
|
token: { state: "unavailable", reason: "permission denied" },
|
|
} satisfies ProcessOwnershipFact;
|
|
expect(
|
|
owner.reconcile([currentParent, unreadableChild], {
|
|
...options,
|
|
listedRows: 2,
|
|
listedPids: new Set([parent.pid, child.pid]),
|
|
}),
|
|
).toMatchObject([
|
|
{ pid: parent.pid, applicability: "token-verified" },
|
|
{
|
|
pid: child.pid,
|
|
applicability: "related-ownership-unresolved",
|
|
scopeEvidence: "current-token-owned-parent",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("does not retain unrelated transient rows and reports table coverage as unknown", () => {
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
const unrelated: ProcessOwnershipFact = {
|
|
process: { ...row, pid: 88, parentPid: 1, processGroupId: 88 },
|
|
state: "same-generation",
|
|
identityBefore: identity("start:unrelated", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 88,
|
|
}),
|
|
token: { state: "unavailable", reason: "temporary permission error" },
|
|
identityAfter: identity("start:unrelated", "non-zombie", {
|
|
state: "observed",
|
|
parentPid: 1,
|
|
processGroupId: 88,
|
|
}),
|
|
};
|
|
expect(
|
|
owner.reconcile([unrelated], {
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([88]),
|
|
leaderPid: 42,
|
|
processGroupIds: new Set([42]),
|
|
}),
|
|
).toEqual([]);
|
|
expect(owner.coverage).toMatchObject({
|
|
listedRows: 1,
|
|
examinedPids: 1,
|
|
retainedUnresolved: 0,
|
|
processTableCoverage: "unreported-by-host",
|
|
});
|
|
});
|
|
|
|
it("preserves the caller's cancellation reason after the final identity read", async () => {
|
|
const controller = new AbortController();
|
|
const reason = new Error("caller stopped final identity read");
|
|
let identityRead = 0;
|
|
const host: ProcessOwnershipHost = {
|
|
...hostFor([
|
|
new Map([[row.pid, identity("start:42")]]),
|
|
new Map([[row.pid, identity("start:42")]]),
|
|
]),
|
|
processIdentities: async () => {
|
|
identityRead += 1;
|
|
if (identityRead === 2) controller.abort(reason);
|
|
return new Map([[row.pid, identity("start:42")]]);
|
|
},
|
|
};
|
|
await expect(
|
|
readProcessOwnershipFacts([row], host, controller.signal),
|
|
).rejects.toMatchObject({
|
|
_tag: "ProcessOwnershipInspectionCancelled",
|
|
reason,
|
|
cause: reason,
|
|
});
|
|
});
|