import { afterEach, describe, expect, it, vi } from
"vitest" ;
import {
initializeGlobalHookRunner,
resetGlobalHookRunner,
} from
"../../plugins/hook-runner-global.js" ;
import { createMockPluginRegistry } from
"../../plugins/hooks.test-helpers.js" ;
import {
__testing,
buildNativeHookRelayCommand,
invokeNativeHookRelay,
registerNativeHookRelay,
} from
"./native-hook-relay.js" ;
afterEach(() => {
vi.useRealTimers();
resetGlobalHookRunner();
__testing.clearNativeHookRelaysForTests();
});
describe(
"native hook relay registry" , () => {
it(
"registers a short-lived relay and builds hidden CLI commands" , () => {
const relay = registerNativeHookRelay({
provider:
"codex" ,
agentId:
"agent-1" ,
sessionId:
"session-1" ,
sessionKey:
"agent:main:session-1" ,
runId:
"run-1" ,
allowedEvents: [
"pre_tool_use" ],
ttlMs:
10 _
000 ,
command: {
executable:
"/opt/Open Claw/openclaw.mjs" ,
nodeExecutable:
"/usr/local/bin/node" ,
timeoutMs:
1234 ,
},
});
expect(__testing.getNativeHookRelayRegistrationForTests(relay.relayId)).toMatchO
bject({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
allowedEvents: ["pre_tool_use" ],
});
expect(relay.commandForEvent("pre_tool_use" )).toBe(
"/usr/local/bin/node '/opt/Open Claw/openclaw.mjs' hooks relay --provider codex --relay-id " +
`${relay.relayId} --event pre_tool_use --timeout 1234 `,
);
});
it("accepts an allowed Codex invocation and preserves raw payload" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
allowedEvents: ["pre_tool_use" ],
});
const response = await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {
hook_event_name: "PreToolUse" ,
cwd: "/repo" ,
model: "gpt-5.4" ,
tool_name: "Bash" ,
tool_use_id: "call-1" ,
tool_input: { command: "pnpm test" },
},
});
expect(response).toEqual({ stdout: "" , stderr: "" , exitCode: 0 });
expect(__testing.getNativeHookRelayInvocationsForTests()).toEqual([
expect.objectContaining({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
nativeEventName: "PreToolUse" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
cwd: "/repo" ,
model: "gpt-5.4" ,
toolName: "Bash" ,
toolUseId: "call-1" ,
rawPayload: expect.objectContaining({
tool_input: { command: "pnpm test" },
}),
}),
]);
});
it("removes retained invocations when a relay is unregistered" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
allowedEvents: ["pre_tool_use" ],
});
await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {
hook_event_name: "PreToolUse" ,
tool_name: "Bash" ,
tool_use_id: "call-1" ,
tool_input: { command: "pnpm test" },
},
});
expect(__testing.getNativeHookRelayInvocationsForTests()).toHaveLength(1 );
relay.unregister();
expect(__testing.getNativeHookRelayRegistrationForTests(relay.relayId)).toBeUndefined();
expect(__testing.getNativeHookRelayInvocationsForTests()).toEqual([]);
});
it("keeps only a bounded history of retained invocations" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
allowedEvents: ["pre_tool_use" ],
});
for (let index = 0 ; index < 210 ; index += 1 ) {
await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {
hook_event_name: "PreToolUse" ,
tool_name: "Bash" ,
tool_use_id: `call-${index}`,
tool_input: { command: `echo ${index}` },
},
});
}
const invocations = __testing.getNativeHookRelayInvocationsForTests();
expect(invocations).toHaveLength(200 );
expect(invocations.some((invocation) => invocation.toolUseId === "call-0" )).toBe(false );
expect(invocations.at(-1 )).toEqual(expect.objectContaining({ toolUseId: "call-209" }));
});
it("rejects missing, wrong-provider, and disallowed-event invocations" , async () => {
await expect(
invokeNativeHookRelay({
provider: "codex" ,
relayId: "missing" ,
event: "pre_tool_use" ,
rawPayload: {},
}),
).rejects.toThrow("not found" );
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
allowedEvents: ["post_tool_use" ],
});
await expect(
invokeNativeHookRelay({
provider: "claude-code" ,
relayId: relay.relayId,
event: "post_tool_use" ,
rawPayload: {},
}),
).rejects.toThrow("unsupported" );
await expect(
invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {},
}),
).rejects.toThrow("not allowed" );
});
it("rejects payloads beyond the relay JSON budget without recursive traversal" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
allowedEvents: ["pre_tool_use" ],
});
let rawPayload: Record<string, unknown> = {};
for (let index = 0 ; index < 80 ; index += 1 ) {
rawPayload = { child: rawPayload };
}
await expect(
invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload,
}),
).rejects.toThrow("JSON-compatible" );
});
it("rejects expired relay ids" , async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-24T12:00:00Z" ));
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
ttlMs: 1 ,
});
vi.setSystemTime(new Date("2026-04-24T12:00:01Z" ));
await expect(
invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {},
}),
).rejects.toThrow("expired" );
expect(__testing.getNativeHookRelayRegistrationForTests(relay.relayId)).toBeUndefined();
});
it("uses the Codex no-op output when no OpenClaw hook decides" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
});
for (const event of ["pre_tool_use" , "post_tool_use" , "permission_request" ] as const ) {
await expect(
invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event,
rawPayload: { hook_event_name: event },
}),
).resolves.toEqual({ stdout: "" , stderr: "" , exitCode: 0 });
}
});
it("maps Codex PreToolUse to OpenClaw before_tool_call and blocks before execution" , async () => {
const beforeToolCall = vi.fn(async () => ({
block: true ,
blockReason: "repo policy blocks this command" ,
}));
initializeGlobalHookRunner(
createMockPluginRegistry([{ hookName: "before_tool_call" , handler: beforeToolCall }]),
);
const relay = registerNativeHookRelay({
provider: "codex" ,
agentId: "agent-1" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
});
const response = await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {
hook_event_name: "PreToolUse" ,
cwd: "/repo" ,
model: "gpt-5.4" ,
tool_name: "Bash" ,
tool_use_id: "native-call-1" ,
tool_input: { command: "rm -rf dist" },
},
});
expect(JSON.parse(response.stdout)).toEqual({
hookSpecificOutput: {
hookEventName: "PreToolUse" ,
permissionDecision: "deny" ,
permissionDecisionReason: "repo policy blocks this command" ,
},
});
expect(response.exitCode).toBe(0 );
expect(beforeToolCall).toHaveBeenCalledWith(
expect.objectContaining({
toolName: "exec" ,
params: { command: "rm -rf dist" },
runId: "run-1" ,
toolCallId: "native-call-1" ,
}),
expect.objectContaining({
agentId: "agent-1" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
toolName: "exec" ,
toolCallId: "native-call-1" ,
}),
);
});
it("does not rewrite Codex native tool input when before_tool_call adjusts params" , async () => {
const beforeToolCall = vi.fn(async () => ({
params: { command: "echo replaced" },
}));
initializeGlobalHookRunner(
createMockPluginRegistry([{ hookName: "before_tool_call" , handler: beforeToolCall }]),
);
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
});
const response = await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "pre_tool_use" ,
rawPayload: {
hook_event_name: "PreToolUse" ,
tool_name: "Bash" ,
tool_use_id: "native-call-1" ,
tool_input: { command: "echo original" },
},
});
expect(response).toEqual({ stdout: "" , stderr: "" , exitCode: 0 });
expect(beforeToolCall).toHaveBeenCalledTimes(1 );
});
it("maps Codex PostToolUse to OpenClaw after_tool_call observation" , async () => {
const afterToolCall = vi.fn();
initializeGlobalHookRunner(
createMockPluginRegistry([{ hookName: "after_tool_call" , handler: afterToolCall }]),
);
const relay = registerNativeHookRelay({
provider: "codex" ,
agentId: "agent-1" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
});
const response = await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "post_tool_use" ,
rawPayload: {
hook_event_name: "PostToolUse" ,
tool_name: "Bash" ,
tool_use_id: "native-call-1" ,
tool_input: { command: "pnpm test" },
tool_response: { output: "ok" , exit_code: 0 },
},
});
expect(response).toEqual({ stdout: "" , stderr: "" , exitCode: 0 });
expect(afterToolCall).toHaveBeenCalledWith(
expect.objectContaining({
toolName: "exec" ,
params: { command: "pnpm test" },
runId: "run-1" ,
toolCallId: "native-call-1" ,
result: { output: "ok" , exit_code: 0 },
}),
expect.objectContaining({
agentId: "agent-1" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
toolName: "exec" ,
toolCallId: "native-call-1" ,
}),
);
});
it("maps PermissionRequest approval allow and deny decisions to Codex hook output" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
agentId: "agent-1" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
});
const approvalRequester = vi
.fn()
.mockResolvedValueOnce("allow" as const )
.mockResolvedValueOnce("deny" as const );
__testing.setNativeHookRelayPermissionApprovalRequesterForTests(approvalRequester);
const allow = await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "permission_request" ,
rawPayload: {
hook_event_name: "PermissionRequest" ,
cwd: "/repo" ,
model: "gpt-5.4" ,
tool_name: "Bash" ,
tool_input: { command: "git push" },
},
});
const deny = await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "permission_request" ,
rawPayload: {
hook_event_name: "PermissionRequest" ,
tool_name: "Bash" ,
tool_input: { command: "curl https://example.com " },
},
});
expect(JSON.parse(allow.stdout)).toEqual({
hookSpecificOutput: {
hookEventName: "PermissionRequest" ,
decision: { behavior: "allow" },
},
});
expect(JSON.parse(deny.stdout)).toEqual({
hookSpecificOutput: {
hookEventName: "PermissionRequest" ,
decision: { behavior: "deny" , message: "Denied by user" },
},
});
expect(approvalRequester).toHaveBeenCalledWith(
expect.objectContaining({
provider: "codex" ,
agentId: "agent-1" ,
sessionId: "session-1" ,
sessionKey: "agent:main:session-1" ,
runId: "run-1" ,
toolName: "exec" ,
cwd: "/repo" ,
model: "gpt-5.4" ,
toolInput: { command: "git push" },
}),
);
});
it("defers PermissionRequest when OpenClaw approval does not decide" , async () => {
__testing.setNativeHookRelayPermissionApprovalRequesterForTests(
vi.fn(async () => "defer" as const ),
);
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
});
await expect(
invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "permission_request" ,
rawPayload: {
hook_event_name: "PermissionRequest" ,
tool_name: "Bash" ,
tool_input: { command: "cargo test" },
},
}),
).resolves.toEqual({ stdout: "" , stderr: "" , exitCode: 0 });
});
it("deduplicates pending PermissionRequest approvals by relay, run, and tool call" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
});
let resolveDecision: ((decision: "allow" ) => void ) | undefined;
const pendingDecision = new Promise<"allow" >((resolve) => {
resolveDecision = resolve;
});
const approvalRequester = vi.fn(() => pendingDecision);
__testing.setNativeHookRelayPermissionApprovalRequesterForTests(approvalRequester);
const payload = {
hook_event_name: "PermissionRequest" ,
tool_name: "Bash" ,
tool_use_id: "native-call-1" ,
tool_input: { command: "git push" },
};
const first = invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "permission_request" ,
rawPayload: payload,
});
const second = invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "permission_request" ,
rawPayload: payload,
});
await Promise.resolve();
expect(approvalRequester).toHaveBeenCalledTimes(1 );
resolveDecision?.("allow" );
const responses = await Promise.all([first, second]);
expect(responses.map((response) => JSON.parse(response.stdout))).toEqual([
{
hookSpecificOutput: {
hookEventName: "PermissionRequest" ,
decision: { behavior: "allow" },
},
},
{
hookSpecificOutput: {
hookEventName: "PermissionRequest" ,
decision: { behavior: "allow" },
},
},
]);
});
it("defers PermissionRequest approvals after the per-relay approval budget is exhausted" , async () => {
const relay = registerNativeHookRelay({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
});
const approvalRequester = vi.fn(async () => "allow" as const );
__testing.setNativeHookRelayPermissionApprovalRequesterForTests(approvalRequester);
const responses = [];
for (let index = 0 ; index < 13 ; index += 1 ) {
responses.push(
await invokeNativeHookRelay({
provider: "codex" ,
relayId: relay.relayId,
event: "permission_request" ,
rawPayload: {
hook_event_name: "PermissionRequest" ,
tool_name: "Bash" ,
tool_use_id: `native -call-${index}`,
tool_input: { command: `echo ${index}` },
},
}),
);
}
expect(approvalRequester).toHaveBeenCalledTimes(12 );
expect(responses.at(-1 )).toEqual({ stdout: "" , stderr: "" , exitCode: 0 });
});
it("sanitizes PermissionRequest approval previews and reports omitted keys" , () => {
expect(
__testing.formatPermissionApprovalDescriptionForTests({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
toolName: "exec" ,
cwd: "/repo\u001b[31m/red\u001b[0m" ,
model: "gpt-5.4\u202edenied" ,
toolInput: {
command: "printf 'ok'\r\n\u001b[31mred\u001b[0m" ,
},
}),
).toBe("Tool: exec\nCwd: /repo/red\nModel: gpt-5.4 denied\nCommand: printf 'ok' red" );
expect(
__testing.formatPermissionApprovalDescriptionForTests({
provider: "codex" ,
sessionId: "session-1" ,
runId: "run-1" ,
toolName: "exec" ,
toolInput: Object.fromEntries(
Array.from({ length: 13 }, (_, index) => [`key-${index}`, index]),
),
}),
).toContain("(1 omitted)" );
});
});
describe("native hook relay command builder" , () => {
it("uses the Codex hook relay command shape" , () => {
expect(
buildNativeHookRelayCommand({
provider: "codex" ,
relayId: "relay-1" ,
event: "permission_request" ,
executable: "openclaw" ,
}),
).toBe(
"openclaw hooks relay --provider codex --relay-id relay-1 --event permission_request --timeout 5000" ,
);
});
});
Messung V0.5 in Prozent C=100 H=95 G=97
¤ Dauer der Verarbeitung: 0.7 Sekunden
¤
*© Formatika GbR, Deutschland