fix(process): integrate main and confirm native exit transitions
This commit is contained in:
commit
8ea965261e
71 files changed
+6651
-475
No files matched your search
+23
-12
@@ -5,15 +5,22 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Route production MCP before importing Incur. Incur owns registration helpers
|
||||
// such as `mcp add`, while the MCP runtime serves the stdio tool catalog.
|
||||
// such as `mcp add`, while the MCP runtime serves the tool catalog over stdio
|
||||
// or, with `--http`, over streamable HTTP.
|
||||
const args = process.argv.slice(2);
|
||||
const { default: packageJson } = await import("../package.json", {
|
||||
with: { type: "json" },
|
||||
});
|
||||
process.env.REA_PACKAGE_VERSION = packageJson.version;
|
||||
const isMcpMode =
|
||||
args.length === 1 && (args[0] === "--mcp" || args[0] === "mcp");
|
||||
const isMcpEntry = args[0] === "--mcp" || args[0] === "mcp";
|
||||
const isMcpMode = isMcpEntry && args.length === 1;
|
||||
const isMcpDoctorMode = args[0] === "mcp" && args[1] === "doctor";
|
||||
// Only flags may follow the entry: `mcp add --http` stays Incur's `mcp add`,
|
||||
// while `mcp -h --http` reaches the HTTP entry's own help.
|
||||
const isMcpHttpMode =
|
||||
isMcpEntry &&
|
||||
(args[1]?.startsWith("-") ?? false) &&
|
||||
args.slice(1).includes("--http");
|
||||
/** Exit status of a writer terminated by SIGPIPE (128 + 13). */
|
||||
const SIGPIPE_EXIT_STATUS = 141;
|
||||
// Set once the reader of CLI output has closed the pipe.
|
||||
@@ -40,15 +47,16 @@ if (
|
||||
process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright");
|
||||
}
|
||||
|
||||
const runtimeFiles = isMcpMode
|
||||
? ["dist/mcp/main.js"]
|
||||
: isMcpDoctorMode
|
||||
? ["dist/mcp/main.js", "dist/cli-runtime/mcpDoctor.js"]
|
||||
: [
|
||||
"dist/cli-runtime/cli.js",
|
||||
"dist/cli-runtime/cliOutput.js",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
];
|
||||
const runtimeFiles =
|
||||
isMcpMode || isMcpHttpMode
|
||||
? ["dist/mcp/main.js"]
|
||||
: isMcpDoctorMode
|
||||
? ["dist/mcp/main.js", "dist/cli-runtime/mcpDoctor.js"]
|
||||
: [
|
||||
"dist/cli-runtime/cli.js",
|
||||
"dist/cli-runtime/cliOutput.js",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
];
|
||||
|
||||
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
process.stderr.write(
|
||||
@@ -58,6 +66,9 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
} else if (isMcpMode) {
|
||||
const { runEntrypoint } = await import("../dist/mcp/main.js");
|
||||
await runEntrypoint();
|
||||
} else if (isMcpHttpMode) {
|
||||
const { runMcpHttpEntrypoint } = await import("../dist/mcp/main.js");
|
||||
await runMcpHttpEntrypoint(args.slice(1));
|
||||
} else if (isMcpDoctorMode) {
|
||||
guardClosedStdout();
|
||||
const { runProductionMcpDoctorCli } =
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** Preserve an unexpected JSON result while describing the failed plan check. */
|
||||
export function setupPlanFailureMessage(options: {
|
||||
readonly planned: unknown;
|
||||
readonly plannedExecution: { readonly status: number | null };
|
||||
readonly plannedClaudeConfig: string;
|
||||
readonly plannedCodexConfig: string;
|
||||
readonly plannedCursorConfig: string;
|
||||
}): string;
|
||||
@@ -0,0 +1,23 @@
|
||||
/** Describe a failed package setup-plan check without masking its result shape. */
|
||||
export const setupPlanFailureMessage = ({
|
||||
planned,
|
||||
plannedExecution,
|
||||
plannedClaudeConfig,
|
||||
plannedCodexConfig,
|
||||
plannedCursorConfig,
|
||||
}) => {
|
||||
const actionKinds = (actions) =>
|
||||
Array.isArray(actions)
|
||||
? actions.map((action) => action?.kind ?? action)
|
||||
: actions;
|
||||
return `packaged setup plan was not read-only: ${JSON.stringify({
|
||||
status: planned?.status,
|
||||
exitCode: plannedExecution.status,
|
||||
plannedKinds: actionKinds(planned?.plannedActions),
|
||||
appliedKinds: actionKinds(planned?.appliedActions),
|
||||
result: planned,
|
||||
claudeConfig: plannedClaudeConfig,
|
||||
codexConfig: plannedCodexConfig,
|
||||
cursorConfig: plannedCursorConfig,
|
||||
})}`;
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
|
||||
import { setupPlanFailureMessage } from "./verify-package-setup-plan.mjs";
|
||||
|
||||
export const terminalModesRestored = (termios) => {
|
||||
if (typeof termios !== "string") return false;
|
||||
@@ -250,23 +251,37 @@ const verifySetupPlan = async ({
|
||||
);
|
||||
const planned = json(plannedExecution.stdout);
|
||||
if (supportedSetupHost) {
|
||||
const plannedActions = Array.isArray(planned?.plannedActions)
|
||||
? planned.plannedActions
|
||||
: undefined;
|
||||
const appliedActions = Array.isArray(planned?.appliedActions)
|
||||
? planned.appliedActions
|
||||
: undefined;
|
||||
const plannedClaudeConfig = await readFile(claudeConfig, "utf8");
|
||||
const plannedCodexConfig = await readFile(codexTarget, "utf8");
|
||||
const plannedCursorConfig = await readFile(cursorConfig, "utf8");
|
||||
const plannedOpenCodeConfig = await readFile(opencodeConfig, "utf8");
|
||||
if (
|
||||
planned.status !== "planned" ||
|
||||
planned?.status !== "planned" ||
|
||||
plannedExecution.status !== 0 ||
|
||||
planned.appliedActions.length !== 0 ||
|
||||
!planned.plannedActions.some(({ kind }) => kind === "configure_client") ||
|
||||
!planned.plannedActions.some(({ kind }) => kind === "install_skill") ||
|
||||
plannedActions === undefined ||
|
||||
appliedActions === undefined ||
|
||||
appliedActions.length !== 0 ||
|
||||
!plannedActions.some((action) => action?.kind === "configure_client") ||
|
||||
!plannedActions.some((action) => action?.kind === "install_skill") ||
|
||||
plannedClaudeConfig !== '{"existing":true}\n' ||
|
||||
plannedCodexConfig !== CODEX_ORIGINAL ||
|
||||
plannedCursorConfig !== '{"existing":true}\n' ||
|
||||
plannedOpenCodeConfig !== opencodeOriginal
|
||||
)
|
||||
throw new Error(
|
||||
`packaged setup plan was not read-only: ${JSON.stringify({ status: planned.status, exitCode: plannedExecution.status, plannedKinds: planned.plannedActions.map(({ kind }) => kind), appliedKinds: planned.appliedActions.map(({ kind }) => kind), claudeConfig: plannedClaudeConfig, codexConfig: plannedCodexConfig, cursorConfig: plannedCursorConfig })}`,
|
||||
setupPlanFailureMessage({
|
||||
planned,
|
||||
plannedExecution,
|
||||
plannedClaudeConfig,
|
||||
plannedCodexConfig,
|
||||
plannedCursorConfig,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return { planned };
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface OwnershipTraceExecution {
|
||||
readonly pid: number;
|
||||
readonly path: string;
|
||||
readonly line: string;
|
||||
readonly succeeded: boolean;
|
||||
readonly ownershipInventory: boolean;
|
||||
}
|
||||
/** Parsed topology and execution facts for one complete strace observation. */
|
||||
export function collectOwnershipTraceFacts(options: {
|
||||
readonly traces: readonly { readonly pid: number; readonly text: string }[];
|
||||
readonly nodeExecutable: string;
|
||||
readonly psExecutables: ReadonlySet<string>;
|
||||
}): {
|
||||
readonly ownershipThreads: ReadonlySet<number>;
|
||||
readonly psProcesses: ReadonlySet<number>;
|
||||
readonly executions: readonly OwnershipTraceExecution[];
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const ownershipPsArguments =
|
||||
'["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]';
|
||||
|
||||
/** Parse execution/thread facts once; only an observed Node ps caller owns inventory reads. */
|
||||
export const collectOwnershipTraceFacts = ({
|
||||
traces,
|
||||
nodeExecutable,
|
||||
psExecutables,
|
||||
}) => {
|
||||
const processGroups = new Map();
|
||||
const findGroup = (pid) => {
|
||||
const parent = processGroups.get(pid);
|
||||
if (parent === undefined) {
|
||||
processGroups.set(pid, pid);
|
||||
return pid;
|
||||
}
|
||||
if (parent === pid) return pid;
|
||||
const root = findGroup(parent);
|
||||
processGroups.set(pid, root);
|
||||
return root;
|
||||
};
|
||||
const joinThreadGroup = (left, right) => {
|
||||
const leftRoot = findGroup(left);
|
||||
const rightRoot = findGroup(right);
|
||||
if (leftRoot !== rightRoot) processGroups.set(rightRoot, leftRoot);
|
||||
};
|
||||
|
||||
const cloneEvents = [];
|
||||
for (const trace of traces) {
|
||||
let unfinished;
|
||||
for (const rawLine of trace.text.split("\n")) {
|
||||
const line = rawLine.trimStart();
|
||||
const start = /^(clone3?|fork|vfork)\((.*)<unfinished \.\.\.>$/u.exec(
|
||||
line,
|
||||
);
|
||||
if (start !== null) {
|
||||
unfinished = { name: start[1], text: start[2] };
|
||||
continue;
|
||||
}
|
||||
const resumed = /^<\.\.\. (clone3?|fork|vfork) resumed>(.*)$/u.exec(line);
|
||||
if (resumed !== null && resumed[1] === unfinished?.name) {
|
||||
cloneEvents.push({
|
||||
parentPid: trace.pid,
|
||||
text: `${unfinished.text}${resumed[2]}`,
|
||||
});
|
||||
unfinished = undefined;
|
||||
continue;
|
||||
}
|
||||
const complete =
|
||||
/^(clone3?|fork|vfork)\((.*\)) = (\d+)(?:<[^>]*>)?$/u.exec(line);
|
||||
if (complete !== null)
|
||||
cloneEvents.push({
|
||||
parentPid: trace.pid,
|
||||
childPid: Number(complete[3]),
|
||||
text: complete[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of cloneEvents) {
|
||||
const childPid =
|
||||
event.childPid ?? /\) = (\d+)(?:<[^>]*>)?$/u.exec(event.text)?.[1];
|
||||
if (childPid === undefined) continue;
|
||||
const observedChild = Number(childPid);
|
||||
// A child-side clone return of zero is not a parent-to-child edge.
|
||||
if (!Number.isSafeInteger(observedChild) || observedChild <= 0) continue;
|
||||
event.childPid = observedChild;
|
||||
if (/\bCLONE_THREAD\b/u.test(event.text))
|
||||
joinThreadGroup(event.parentPid, event.childPid);
|
||||
}
|
||||
|
||||
const executions = [];
|
||||
for (const trace of traces) {
|
||||
for (const line of trace.text.split("\n")) {
|
||||
if (!/execve(?:at)?\(/u.test(line)) continue;
|
||||
const match = /^execve\("([^"]+)"/u.exec(line);
|
||||
assert.notEqual(match, null, `Unresolved executable identity: ${line}`);
|
||||
assert.ok(
|
||||
/ = (?:0|-1 [A-Z]+)/u.test(line),
|
||||
`Incomplete execution observation: ${line}`,
|
||||
);
|
||||
executions.push({
|
||||
pid: trace.pid,
|
||||
path: match[1],
|
||||
line,
|
||||
succeeded: line.endsWith(" = 0"),
|
||||
ownershipInventory:
|
||||
psExecutables.has(match[1]) && line.includes(ownershipPsArguments),
|
||||
});
|
||||
}
|
||||
}
|
||||
const psProcesses = new Set(
|
||||
executions
|
||||
.filter(
|
||||
({ succeeded, ownershipInventory }) => succeeded && ownershipInventory,
|
||||
)
|
||||
.map(({ pid }) => pid),
|
||||
);
|
||||
const psParentGroups = new Set(
|
||||
cloneEvents
|
||||
.filter(({ childPid }) => psProcesses.has(childPid))
|
||||
.map(({ parentPid }) => findGroup(parentPid)),
|
||||
);
|
||||
const nodeGroups = new Set(
|
||||
executions
|
||||
.filter(({ path, succeeded }) => succeeded && path === nodeExecutable)
|
||||
.map(({ pid }) => findGroup(pid)),
|
||||
);
|
||||
const replaced = new Set(
|
||||
executions
|
||||
.filter(({ path, succeeded }) => succeeded && path !== nodeExecutable)
|
||||
.map(({ pid }) => pid),
|
||||
);
|
||||
const ownershipThreads = new Set(
|
||||
traces
|
||||
.filter(
|
||||
({ pid }) =>
|
||||
nodeGroups.has(findGroup(pid)) &&
|
||||
psParentGroups.has(findGroup(pid)) &&
|
||||
!replaced.has(pid),
|
||||
)
|
||||
.map(({ pid }) => pid),
|
||||
);
|
||||
return { ownershipThreads, psProcesses, executions };
|
||||
};
|
||||
@@ -7,14 +7,10 @@ export const historicalPidTraceContract: {
|
||||
readonly pid_syscalls: readonly string[];
|
||||
readonly unverified: readonly string[];
|
||||
};
|
||||
export function nodeInventoryThreads(
|
||||
traces: readonly SyscallTrace[],
|
||||
executable: string,
|
||||
): ReadonlySet<number>;
|
||||
export function assertHistoricalPidAccess(input: {
|
||||
readonly trace: SyscallTrace;
|
||||
readonly historicalPid: number;
|
||||
readonly nodeThreads: ReadonlySet<number>;
|
||||
readonly ownershipThreads: ReadonlySet<number>;
|
||||
readonly liveCollision: boolean;
|
||||
readonly psInventory: boolean;
|
||||
}): readonly string[];
|
||||
@@ -1,52 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { posix } from "node:path";
|
||||
|
||||
/** Identify REA's Node processes and their observed CLONE_THREAD children. */
|
||||
export function nodeInventoryThreads(traces, executable) {
|
||||
const executesForeign = (text) =>
|
||||
text
|
||||
.split("\n")
|
||||
.some(
|
||||
(line) =>
|
||||
/^execve\(/u.test(line) &&
|
||||
line.endsWith(" = 0") &&
|
||||
!line.startsWith(`execve(${JSON.stringify(executable)},`),
|
||||
);
|
||||
const byPid = new Map(traces.map((trace) => [trace.pid, trace]));
|
||||
const nodes = new Set(
|
||||
traces
|
||||
.filter(
|
||||
({ text }) =>
|
||||
!executesForeign(text) &&
|
||||
text
|
||||
.split("\n")
|
||||
.some(
|
||||
(line) =>
|
||||
line.startsWith(`execve(${JSON.stringify(executable)},`) &&
|
||||
line.endsWith(" = 0"),
|
||||
),
|
||||
)
|
||||
.map(({ pid }) => pid),
|
||||
);
|
||||
for (const pid of nodes) {
|
||||
let pendingThreadClone = false;
|
||||
for (const line of byPid.get(pid)?.text.split("\n") ?? []) {
|
||||
if (!/^(?:clone3?\(|<\.\.\. clone3? resumed>)/u.test(line)) continue;
|
||||
if (/^clone3?\(/u.test(line))
|
||||
pendingThreadClone = /\bCLONE_THREAD\b/u.test(line);
|
||||
const child = / = (\d+)$/u.exec(line);
|
||||
if (pendingThreadClone && child !== null) {
|
||||
const childPid = Number(child[1]);
|
||||
const trace = byPid.get(childPid);
|
||||
if (trace !== undefined && !executesForeign(trace.text))
|
||||
nodes.add(childPid);
|
||||
}
|
||||
if (!line.endsWith("<unfinished ...>")) pendingThreadClone = false;
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// This owner supplies both tracer selection and target interpretation. Kinds
|
||||
// describe syscall argument semantics, never catalog positions or output PIDs.
|
||||
const pidTargetKinds = new Map([
|
||||
@@ -176,7 +130,7 @@ const readsMetadata = (line, root, names) => {
|
||||
export function assertHistoricalPidAccess({
|
||||
trace,
|
||||
historicalPid,
|
||||
nodeThreads,
|
||||
ownershipThreads,
|
||||
liveCollision,
|
||||
psInventory,
|
||||
}) {
|
||||
@@ -216,8 +170,9 @@ export function assertHistoricalPidAccess({
|
||||
!referencesHistoricalPath(line)
|
||||
)
|
||||
continue;
|
||||
const nodeStat =
|
||||
nodeThreads.has(trace.pid) && readsMetadata(line, root, ["stat"]);
|
||||
const ownershipMetadata =
|
||||
ownershipThreads.has(trace.pid) &&
|
||||
readsMetadata(line, root, ["stat", "status"]);
|
||||
const psMetadata =
|
||||
psInventory &&
|
||||
(readsMetadata(line, root, [
|
||||
@@ -236,7 +191,7 @@ export function assertHistoricalPidAccess({
|
||||
liveCollision &&
|
||||
target !== "historical" &&
|
||||
target !== "unresolved" &&
|
||||
(nodeStat || psMetadata),
|
||||
(ownershipMetadata || psMetadata),
|
||||
`${target === "unresolved" ? "Unresolved process target in" : "Historical PID caused a"} non-inventory lookup in trace ${trace.pid}: ${line}`,
|
||||
);
|
||||
accesses.push(line);
|
||||
|
||||
@@ -7,8 +7,8 @@ import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import {
|
||||
assertHistoricalPidAccess,
|
||||
historicalPidTraceContract,
|
||||
nodeInventoryThreads,
|
||||
} from "./trace-policy.mjs";
|
||||
import { collectOwnershipTraceFacts } from "./trace-ownership.mjs";
|
||||
|
||||
/** Check attempted syscalls, including failed attempts, on a recorded PID that collides with an owned live sentinel. */
|
||||
export async function verifyCoreInspectionTrace({
|
||||
@@ -100,11 +100,19 @@ export async function verifyCoreInspectionTrace({
|
||||
// Retain only execution/thread evidence across files, not all raw traces.
|
||||
const inventory = text
|
||||
.split("\n")
|
||||
.filter((line) => /^(?:execve|clone|<\.\.\. clone)/u.test(line))
|
||||
.filter((line) =>
|
||||
/^(?:execve|clone|fork|vfork|<\.\.\. (?:clone|fork|vfork))/u.test(
|
||||
line,
|
||||
),
|
||||
)
|
||||
.join("\n");
|
||||
traces.push({ pid, text: inventory, file });
|
||||
}
|
||||
const nodeThreads = nodeInventoryThreads(traces, process.execPath);
|
||||
const ownershipFacts = collectOwnershipTraceFacts({
|
||||
traces,
|
||||
nodeExecutable: process.execPath,
|
||||
psExecutables: psPaths,
|
||||
});
|
||||
for (const trace of traces) {
|
||||
const { file } = trace;
|
||||
const text = await readFile(join(root, file), "utf8");
|
||||
@@ -115,23 +123,13 @@ export async function verifyCoreInspectionTrace({
|
||||
!text.split("\n").some((line) => line.includes(recordedExecutable)),
|
||||
`Inspection attempted access to the recorded executable: ${file}`,
|
||||
);
|
||||
const ownershipTrace = text
|
||||
.split("\n")
|
||||
.some(
|
||||
(line) =>
|
||||
/^execve\("\/(?:usr\/)?bin\/ps",/u.test(line) &&
|
||||
line.includes(
|
||||
'["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]',
|
||||
) &&
|
||||
line.endsWith(" = 0"),
|
||||
);
|
||||
ownershipReads.push(
|
||||
...assertHistoricalPidAccess({
|
||||
trace: { pid: trace.pid, text },
|
||||
historicalPid,
|
||||
nodeThreads,
|
||||
ownershipThreads: ownershipFacts.ownershipThreads,
|
||||
liveCollision: historicalPid === sentinel.pid,
|
||||
psInventory: ownershipTrace,
|
||||
psInventory: ownershipFacts.psProcesses.has(trace.pid),
|
||||
}),
|
||||
);
|
||||
assert.ok(
|
||||
@@ -148,48 +146,40 @@ export async function verifyCoreInspectionTrace({
|
||||
),
|
||||
`Inspection attempted an Internet socket: ${file}`,
|
||||
);
|
||||
for (const line of text.split("\n")) {
|
||||
if (!/execve(?:at)?\(/u.test(line)) continue;
|
||||
const match = /^execve\("([^"]+)"/u.exec(line);
|
||||
assert.notEqual(match, null, `Unresolved executable identity: ${line}`);
|
||||
assert.ok(
|
||||
/ = (?:0|-1 [A-Z]+)/u.test(line),
|
||||
`Incomplete execution observation: ${line}`,
|
||||
}
|
||||
for (const execution of ownershipFacts.executions) {
|
||||
const { line } = execution;
|
||||
const executable = await realpath(execution.path).catch(
|
||||
() => execution.path,
|
||||
);
|
||||
const ownership = execution.ownershipInventory;
|
||||
const iconv =
|
||||
iconvPaths.has(execution.path) && line.includes('["iconv", "-l"]');
|
||||
const version =
|
||||
gitPaths.has(execution.path) &&
|
||||
line.includes(
|
||||
"[" +
|
||||
[
|
||||
"git",
|
||||
"--git-dir",
|
||||
join(plugin, ".git"),
|
||||
"rev-parse",
|
||||
"--short",
|
||||
"HEAD",
|
||||
]
|
||||
.map((argument) => JSON.stringify(argument))
|
||||
.join(", ") +
|
||||
"]",
|
||||
);
|
||||
const executable = await realpath(match[1]).catch(() => match[1]);
|
||||
const ownership =
|
||||
psPaths.has(match[1]) &&
|
||||
line.includes(
|
||||
'["ps", "-axo", "pid=,ppid=,pgid=,uid=,stat=,command="]',
|
||||
);
|
||||
const iconv =
|
||||
iconvPaths.has(match[1]) && line.includes('["iconv", "-l"]');
|
||||
const version =
|
||||
gitPaths.has(match[1]) &&
|
||||
line.includes(
|
||||
"[" +
|
||||
[
|
||||
"git",
|
||||
"--git-dir",
|
||||
join(plugin, ".git"),
|
||||
"rev-parse",
|
||||
"--short",
|
||||
"HEAD",
|
||||
]
|
||||
.map((argument) => JSON.stringify(argument))
|
||||
.join(", ") +
|
||||
"]",
|
||||
);
|
||||
assert.ok(
|
||||
[process.execPath, python, gdb].includes(executable) ||
|
||||
ownership ||
|
||||
iconv ||
|
||||
version,
|
||||
`Unexpected attempted host execution: ${line}`,
|
||||
);
|
||||
if (executable === gdb) gdbObserved = true;
|
||||
executions.push(executable);
|
||||
}
|
||||
assert.ok(
|
||||
[process.execPath, python, gdb].includes(executable) ||
|
||||
ownership ||
|
||||
iconv ||
|
||||
version,
|
||||
`Unexpected attempted host execution: ${line}`,
|
||||
);
|
||||
if (executable === gdb) gdbObserved = true;
|
||||
executions.push(executable);
|
||||
}
|
||||
assert.ok(gdbObserved, "Trace must include the actual core-only GDB stage");
|
||||
assert.ok(executions.includes(python));
|
||||
|
||||
Reference in new issue
Block a user