import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
type StrictInlineEvalBoundary =
typeof import ("./bash-tools.exec-host-shared.js" ).enforceStrictInlineEvalApprovalBoundary;
const INLINE_EVAL_HIT = {
executable: "python3" ,
normalizedExecutable: "python3" ,
flag: "-c" ,
argv: ["python3" , "-c" , "print(1)" ],
};
const preparedPlan = vi.hoisted(() => ({
argv: ["bun" , "./script.ts" ],
cwd: "/tmp/work" ,
commandText: "bun ./script.ts" ,
commandPreview: "bun ./script.ts" ,
agentId: "prepared-agent" ,
sessionKey: "prepared-session" ,
mutableFileOperand: {
argvIndex: 1 ,
path: "/tmp/work/script.ts" ,
sha256: "abc123" ,
},
}));
const callGatewayToolMock = vi.hoisted(() => vi.fn());
const listNodesMock = vi.hoisted(() => vi.fn());
const parsePreparedSystemRunPayloadMock = vi.hoisted(() => vi.fn());
const requiresExecApprovalMock = vi.hoisted(() => vi.fn(() => true ));
const resolveExecHostApprovalContextMock = vi.hoisted(() =>
vi.fn(() => ({
approvals: { allowlist: [], file: { version: 1 , agents: {} } },
hostSecurity: "full" ,
hostAsk: "off" ,
askFallback: "deny" ,
})),
);
const createAndRegisterDefaultExecApprovalRequestMock = vi.hoisted(() => vi.fn());
const resolveApprovalDecisionOrUndefinedMock = vi.hoisted(() =>
vi.fn(async (): Promise<string | null | undefined> => "allow-once" ),
);
const createExecApprovalDecisionStateMock = vi.hoisted(() =>
vi.fn(
(): {
baseDecision: { timedOut: boolean };
approvedByAsk: boolean ;
deniedReason: string | null ;
} => ({
baseDecision: { timedOut: false },
approvedByAsk: false ,
deniedReason: null ,
}),
),
);
const buildExecApprovalPendingToolResultMock = vi.hoisted(() => vi.fn());
const sendExecApprovalFollowupResultMock = vi.hoisted(() => vi.fn(async () => undefined));
const enforceStrictInlineEvalApprovalBoundaryMock = vi.hoisted(() =>
vi.fn<StrictInlineEvalBoundary>((value) => ({
approvedByAsk: value.approvedByAsk,
deniedReason: value.deniedReason,
})),
);
const registerExecApprovalRequestForHostOrThrowMock = vi.hoisted(() =>
vi.fn(async () => undefined),
);
const detectInterpreterInlineEvalArgvMock = vi.hoisted(() =>
vi.fn(
(): {
executable: string;
normalizedExecutable: string;
flag: string;
argv: string[];
} | null => null ,
),
);
vi.mock("../infra/exec-approvals.js" , () => ({
evaluateShellAllowlist: vi.fn(() => ({
allowlistMatches: [],
analysisOk: true ,
allowlistSatisfied: false ,
segments: [{ resolution: null , argv: ["bun" , "./script.ts" ] }],
segmentAllowlistEntries: [],
})),
hasDurableExecApproval: vi.fn(() => false ),
requiresExecApproval: requiresExecApprovalMock,
resolveExecApprovalAllowedDecisions: vi.fn(() => ["allow-once" , "allow-always" , "deny" ]),
resolveExecApprovalsFromFile: vi.fn(() => ({
allowlist: [],
file: { version: 1 , agents: {} },
})),
}));
vi.mock("../infra/exec-inline-eval.js" , () => ({
describeInterpreterInlineEval: vi.fn(() => "inline-eval" ),
detectInterpreterInlineEvalArgv: detectInterpreterInlineEvalArgvMock,
}));
vi.mock("../infra/node-shell.js" , () => ({
buildNodeShellCommand: vi.fn(() => ["bash" , "-lc" , "bun ./script.ts" ]),
}));
vi.mock("../infra/system-run-approval-context.js" , () => ({
parsePreparedSystemRunPayload: parsePreparedSystemRunPayloadMock,
}));
vi.mock("./bash-tools.exec-approval-request.js" , () => ({
buildExecApprovalRequesterContext: vi.fn(() => ({})),
buildExecApprovalTurnSourceContext: vi.fn(() => ({})),
registerExecApprovalRequestForHostOrThrow: registerExecApprovalRequestForHostOrThrowMock,
}));
vi.mock("./bash-tools.exec-host-shared.js" , () => ({
resolveExecHostApprovalContext: resolveExecHostApprovalContextMock,
buildDefaultExecApprovalRequestArgs: vi.fn(() => ({})),
createAndRegisterDefaultExecApprovalRequest: createAndRegisterDefaultExecApprovalRequestMock,
shouldResolveExecApprovalUnavailableInline: vi.fn(() => false ),
buildExecApprovalFollowupTarget: vi.fn(() => ({ approvalId: "approval-1" })),
resolveApprovalDecisionOrUndefined: resolveApprovalDecisionOrUndefinedMock,
createExecApprovalDecisionState: createExecApprovalDecisionStateMock,
enforceStrictInlineEvalApprovalBoundary: enforceStrictInlineEvalApprovalBoundaryMock,
sendExecApprovalFollowupResult: sendExecApprovalFollowupResultMock,
buildExecApprovalPendingToolResult: buildExecApprovalPendingToolResultMock,
buildHeadlessExecApprovalDeniedMessage: vi.fn(() => "denied" ),
}));
vi.mock("./bash-tools.exec-runtime.js" , () => ({
DEFAULT_NOTIFY_TAIL_CHARS: 1000 ,
createApprovalSlug: vi.fn(() => "slug" ),
normalizeNotifyOutput: vi.fn((value: string) => value),
}));
vi.mock("./tools/gateway.js" , () => ({
callGatewayTool: callGatewayToolMock,
}));
vi.mock("./tools/nodes-utils.js" , () => ({
listNodes: listNodesMock,
resolveNodeIdFromList: vi.fn(() => "node-1" ),
}));
vi.mock("../logger.js" , () => ({
logInfo: vi.fn(),
}));
let executeNodeHostCommand: typeof import ("./bash-tools.exec-host-node.js" ).executeNodeHostCommand;
type MockNodeInvokeParams = {
command?: string;
params?: Record<string, unknown>;
};
describe("executeNodeHostCommand" , () => {
beforeAll(async () => {
({ executeNodeHostCommand } = await import ("./bash-tools.exec-host-node.js" ));
});
beforeEach(() => {
callGatewayToolMock.mockReset();
callGatewayToolMock.mockImplementation(
async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => {
if (method !== "node.invoke" ) {
throw new Error(`unexpected gateway method: ${method}`);
}
if (params?.command === "system.run.prepare" ) {
return { payload: { plan: preparedPlan } };
}
if (params?.command === "system.run" ) {
return {
payload: {
success: true ,
stdout: "ok" ,
stderr: "" ,
exitCode: 0 ,
timedOut: false ,
},
};
}
throw new Error(`unexpected node invoke command: ${String(params?.command)}`);
},
);
listNodesMock.mockReset();
listNodesMock.mockResolvedValue([
{ nodeId: "node-1" , commands: ["system.run" ], platform: process.platform },
]);
parsePreparedSystemRunPayloadMock.mockReset();
parsePreparedSystemRunPayloadMock.mockReturnValue({ plan: preparedPlan });
requiresExecApprovalMock.mockReset();
requiresExecApprovalMock.mockReturnValue(true );
resolveExecHostApprovalContextMock.mockReset();
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1 , agents: {} } },
hostSecurity: "full" ,
hostAsk: "off" ,
askFallback: "deny" ,
});
createAndRegisterDefaultExecApprovalRequestMock.mockReset();
createAndRegisterDefaultExecApprovalRequestMock.mockImplementation(async (args?: unknown) => {
const register =
args && typeof args === "object" && "register" in args
? (args as { register?: (approvalId: string) => Promise<void > }).register
: undefined;
await register?.("approval-1" );
return {
approvalId: "approval-1" ,
approvalSlug: "slug-1" ,
warningText: "" ,
expiresAtMs: Date.now() + 60 _000 ,
preResolvedDecision: null ,
initiatingSurface: "origin" ,
sentApproverDms: false ,
unavailableReason: null ,
};
});
resolveApprovalDecisionOrUndefinedMock.mockReset();
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue("allow-once" );
createExecApprovalDecisionStateMock.mockReset();
createExecApprovalDecisionStateMock.mockReturnValue({
baseDecision: { timedOut: false },
approvedByAsk: false ,
deniedReason: null ,
});
buildExecApprovalPendingToolResultMock.mockReset();
buildExecApprovalPendingToolResultMock.mockReturnValue({
content: [],
details: { status: "approval-pending" },
});
sendExecApprovalFollowupResultMock.mockReset();
enforceStrictInlineEvalApprovalBoundaryMock.mockReset();
enforceStrictInlineEvalApprovalBoundaryMock.mockImplementation((value) => ({
approvedByAsk: value.approvedByAsk,
deniedReason: value.deniedReason,
}));
detectInterpreterInlineEvalArgvMock.mockReset();
detectInterpreterInlineEvalArgvMock.mockReturnValue(null );
registerExecApprovalRequestForHostOrThrowMock.mockReset();
});
it("forwards prepared systemRunPlan on async node invoke after approval" , async () => {
const result = await executeNodeHostCommand({
command: "bun ./script.ts" ,
workdir: "/tmp/work" ,
env: {},
security: "full" ,
ask: "off" ,
defaultTimeoutSec: 30 ,
approvalRunningNoticeMs: 0 ,
warnings: [],
agentId: "requested-agent" ,
sessionKey: "requested-session" ,
});
expect(result.details?.status).toBe("approval-pending" );
expect(registerExecApprovalRequestForHostOrThrowMock).toHaveBeenCalledWith(
expect.objectContaining({
systemRunPlan: preparedPlan,
}),
);
await vi.waitFor(() => {
expect(callGatewayToolMock).toHaveBeenCalledTimes(2 );
});
expect(callGatewayToolMock).toHaveBeenNthCalledWith(
2 ,
"node.invoke" ,
expect.anything(),
expect.objectContaining({
command: "system.run" ,
params: expect.objectContaining({
approved: true ,
approvalDecision: "allow-once" ,
systemRunPlan: preparedPlan,
}),
}),
);
});
it("suppresses node completion events when notifyOnExit is disabled" , async () => {
requiresExecApprovalMock.mockReturnValue(false );
await executeNodeHostCommand({
command: "bun ./script.ts" ,
workdir: "/tmp/work" ,
env: {},
security: "full" ,
ask: "off" ,
defaultTimeoutSec: 30 ,
approvalRunningNoticeMs: 0 ,
warnings: [],
agentId: "requested-agent" ,
sessionKey: "requested-session" ,
notifyOnExit: false ,
});
expect(callGatewayToolMock).toHaveBeenNthCalledWith(
2 ,
"node.invoke" ,
expect.anything(),
expect.objectContaining({
command: "system.run" ,
params: expect.objectContaining({
suppressNotifyOnExit: true ,
}),
}),
);
});
it("denies timed-out inline-eval requests instead of invoking the node" , async () => {
detectInterpreterInlineEvalArgvMock.mockReturnValue(INLINE_EVAL_HIT);
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(null );
createExecApprovalDecisionStateMock.mockReturnValue({
baseDecision: { timedOut: true },
approvedByAsk: true ,
deniedReason: null ,
});
enforceStrictInlineEvalApprovalBoundaryMock.mockReturnValue({
approvedByAsk: false ,
deniedReason: "approval-timeout" ,
});
resolveExecHostApprovalContextMock.mockReturnValue({
approvals: { allowlist: [], file: { version: 1 , agents: {} } },
hostSecurity: "full" ,
hostAsk: "off" ,
askFallback: "full" ,
});
const result = await executeNodeHostCommand({
command: "python3 -c 'print(1)'" ,
workdir: "/tmp/work" ,
env: {},
security: "full" ,
ask: "off" ,
strictInlineEval: true ,
defaultTimeoutSec: 30 ,
approvalRunningNoticeMs: 0 ,
warnings: [],
agentId: "requested-agent" ,
sessionKey: "requested-session" ,
});
expect(result.details?.status).toBe("approval-pending" );
await vi.waitFor(() => {
expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledWith(
{ approvalId: "approval-1" },
"Exec denied (node=node-1 id=approval-1, approval-timeout): python3 -c 'print(1)'" ,
);
});
expect(callGatewayToolMock).toHaveBeenCalledTimes(1 );
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland