Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
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",
);
});
});
¤ Dauer der Verarbeitung: 0.2 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|