import { describe, expect, it, vi } from "vitest" ;
import {
createChannelApprovalHandlerFromCapability,
createLazyChannelApprovalNativeRuntimeAdapter,
} from "./approval-handler-runtime.js" ;
import {
createApprovalNativeRuntimeAdapterStubs,
type ApprovalNativeRuntimeAdapterStubParams,
} from "./approval-handler.test-helpers.js" ;
import type { ExecApprovalRequest } from "./exec-approvals.js" ;
type ApprovalCapability = NonNullable<
Parameters<typeof createChannelApprovalHandlerFromCapability>[0 ]["capability" ]
>;
type ApprovalNativeAdapter = NonNullable<ApprovalCapability["native" ]>;
const TEST_HANDLER_PARAMS = {
label: "test/approval-handler" ,
clientDisplayName: "Test Approval Handler" ,
channel: "test" ,
channelLabel: "Test" ,
cfg: { channels: {} } as never,
} as const ;
function makeSequentialPendingDeliveryMock() {
return vi
.fn()
.mockResolvedValueOnce({ messageId: "1" })
.mockResolvedValueOnce({ messageId: "2" });
}
function makeSequentialPendingBindingMock() {
return vi
.fn()
.mockResolvedValueOnce({ bindingId: "bound-1" })
.mockResolvedValueOnce({ bindingId: "bound-2" });
}
function makeExecApprovalRequest(id: string): ExecApprovalRequest {
return {
id,
expiresAtMs: Date.now() + 60 _000 ,
request: {
command: "echo hi" ,
turnSourceChannel: "test" ,
turnSourceTo: "origin-chat" ,
},
createdAtMs: Date.now(),
};
}
function makeNativeApprovalCapability(
params: {
preferredSurface?: ReturnType<
ApprovalNativeAdapter["describeDeliveryCapabilities" ]
>["preferredSurface" ];
supportsApproverDmSurface?: boolean ;
resolveApproverDmTargets?: ApprovalNativeAdapter["resolveApproverDmTargets" ];
} & ApprovalNativeRuntimeAdapterStubParams = {},
): ApprovalCapability {
const preferredSurface = params.preferredSurface ?? "origin" ;
return {
native : {
describeDeliveryCapabilities: vi.fn().mockReturnValue({
enabled: true ,
preferredSurface,
supportsOriginSurface: true ,
supportsApproverDmSurface: params.supportsApproverDmSurface ?? false ,
notifyOriginWhenDmOnly: false ,
}),
resolveOriginTarget: vi.fn().mockReturnValue({ to: "origin-chat" }),
...(params.resolveApproverDmTargets
? { resolveApproverDmTargets: params.resolveApproverDmTargets }
: {}),
},
nativeRuntime: createApprovalNativeRuntimeAdapterStubs(params),
};
}
function createTestApprovalHandler(capability: ApprovalCapability) {
return createChannelApprovalHandlerFromCapability({
capability,
...TEST_HANDLER_PARAMS,
});
}
describe("createChannelApprovalHandlerFromCapability" , () => {
it("returns null when the capability does not expose a native runtime" , async () => {
await expect(
createChannelApprovalHandlerFromCapability({
capability: {},
...TEST_HANDLER_PARAMS,
}),
).resolves.toBeNull();
});
it("returns a runtime when the capability exposes a native runtime" , async () => {
const runtime = await createChannelApprovalHandlerFromCapability({
capability: {
nativeRuntime: {
availability: {
isConfigured: vi.fn().mockReturnValue(true ),
shouldHandle: vi.fn().mockReturnValue(true ),
},
presentation: {
buildPendingPayload: vi.fn(),
buildResolvedResult: vi.fn(),
buildExpiredResult: vi.fn(),
},
transport: {
prepareTarget: vi.fn(),
deliverPending: vi.fn(),
},
},
},
...TEST_HANDLER_PARAMS,
});
expect(runtime).not.toBeNull();
});
it("preserves the original request and resolved approval kind when stop-time cleanup unbinds" , async () => {
const unbindPending = vi.fn();
const runtime = await createTestApprovalHandler(
makeNativeApprovalCapability({
resolveApprovalKind: vi.fn().mockReturnValue("plugin" ),
unbindPending,
}),
);
expect(runtime).not.toBeNull();
const request = {
id: "custom:1" ,
expiresAtMs: Date.now() + 60 _000 ,
request: {
turnSourceChannel: "test" ,
turnSourceTo: "origin-chat" ,
},
} as never;
await runtime?.handleRequested(request);
await runtime?.stop();
expect(unbindPending).toHaveBeenCalledWith(
expect.objectContaining({
request,
approvalKind: "plugin" ,
}),
);
});
it("ignores duplicate pending request ids before finalization" , async () => {
const unbindPending = vi.fn();
const buildResolvedResult = vi.fn().mockResolvedValue({ kind: "leave" });
const runtime = await createTestApprovalHandler(
makeNativeApprovalCapability({
buildResolvedResult,
deliverPending: makeSequentialPendingDeliveryMock(),
bindPending: makeSequentialPendingBindingMock(),
unbindPending,
}),
);
expect(runtime).not.toBeNull();
const request = makeExecApprovalRequest("exec:1" );
await runtime?.handleRequested(request);
await runtime?.handleRequested(request);
await runtime?.handleResolved({
id: "exec:1" ,
decision: "approved" ,
resolvedBy: "operator" ,
} as never);
expect(unbindPending).toHaveBeenCalledTimes(1 );
expect(unbindPending).toHaveBeenCalledWith(
expect.objectContaining({
entry: { messageId: "1" },
binding: { bindingId: "bound-1" },
request,
}),
);
expect(buildResolvedResult).toHaveBeenCalledTimes(1 );
});
it("continues finalization cleanup after one resolved entry unbind failure" , async () => {
const unbindPending = vi
.fn()
.mockRejectedValueOnce(new Error("unbind failed" ))
.mockResolvedValueOnce(undefined);
const buildResolvedResult = vi.fn().mockResolvedValue({ kind: "leave" });
const runtime = await createTestApprovalHandler(
makeNativeApprovalCapability({
preferredSurface: "both" ,
supportsApproverDmSurface: true ,
resolveApproverDmTargets: vi.fn().mockResolvedValue([{ to: "approver-dm" }]),
buildResolvedResult,
prepareTarget: vi.fn().mockImplementation(async ({ plannedTarget }) => ({
dedupeKey: String(plannedTarget.target.to),
target: { to: plannedTarget.target.to },
})),
deliverPending: makeSequentialPendingDeliveryMock(),
bindPending: makeSequentialPendingBindingMock(),
unbindPending,
}),
);
const request = makeExecApprovalRequest("exec:2" );
await runtime?.handleRequested(request);
await expect(
runtime?.handleResolved({
id: "exec:2" ,
decision: "approved" ,
resolvedBy: "operator" ,
} as never),
).resolves.toBeUndefined();
expect(unbindPending).toHaveBeenCalledTimes(2 );
expect(buildResolvedResult).toHaveBeenCalledTimes(1 );
expect(buildResolvedResult).toHaveBeenCalledWith(
expect.objectContaining({
entry: { messageId: "2" },
}),
);
});
it("continues stop-time unbind cleanup when one binding throws" , async () => {
const unbindPending = vi
.fn()
.mockRejectedValueOnce(new Error("unbind failed" ))
.mockResolvedValueOnce(undefined);
const runtime = await createTestApprovalHandler(
makeNativeApprovalCapability({
deliverPending: makeSequentialPendingDeliveryMock(),
bindPending: makeSequentialPendingBindingMock(),
unbindPending,
}),
);
const request = makeExecApprovalRequest("exec:stop-1" );
await runtime?.handleRequested(request);
await runtime?.handleRequested({
...request,
id: "exec:stop-2" ,
});
await expect(runtime?.stop()).resolves.toBeUndefined();
expect(unbindPending).toHaveBeenCalledTimes(2 );
await expect(runtime?.stop()).resolves.toBeUndefined();
expect(unbindPending).toHaveBeenCalledTimes(2 );
});
});
describe("createLazyChannelApprovalNativeRuntimeAdapter" , () => {
it("loads the runtime lazily and reuses the loaded adapter" , async () => {
const explicitIsConfigured = vi.fn().mockReturnValue(true );
const explicitShouldHandle = vi.fn().mockReturnValue(false );
const buildPendingPayload = vi.fn().mockResolvedValue({ text: "pending" });
const load = vi.fn().mockResolvedValue({
availability: {
isConfigured: vi.fn(),
shouldHandle: vi.fn(),
},
presentation: {
buildPendingPayload,
buildResolvedResult: vi.fn(),
buildExpiredResult: vi.fn(),
},
transport: {
prepareTarget: vi.fn(),
deliverPending: vi.fn(),
},
});
const adapter = createLazyChannelApprovalNativeRuntimeAdapter({
eventKinds: ["exec" ],
isConfigured: explicitIsConfigured,
shouldHandle: explicitShouldHandle,
load,
});
const cfg = { channels: {} } as never;
const request = { id: "exec:1" } as never;
const view = {} as never;
expect(adapter.eventKinds).toEqual(["exec" ]);
expect(adapter.availability.isConfigured({ cfg })).toBe(true );
expect(adapter.availability.shouldHandle({ cfg, request })).toBe(false );
await expect(
adapter.presentation.buildPendingPayload({
cfg,
request,
approvalKind: "exec" ,
nowMs: 1 ,
view,
}),
).resolves.toEqual({ text: "pending" });
expect(load).toHaveBeenCalledTimes(1 );
expect(explicitIsConfigured).toHaveBeenCalledWith({ cfg });
expect(explicitShouldHandle).toHaveBeenCalledWith({ cfg, request });
expect(buildPendingPayload).toHaveBeenCalledWith({
cfg,
request,
approvalKind: "exec" ,
nowMs: 1 ,
view,
});
});
it("keeps observe hooks synchronous and only uses the already-loaded runtime" , async () => {
const onDelivered = vi.fn();
const load = vi.fn().mockResolvedValue({
availability: {
isConfigured: vi.fn(),
shouldHandle: vi.fn(),
},
presentation: {
buildPendingPayload: vi.fn().mockResolvedValue({ text: "pending" }),
buildResolvedResult: vi.fn(),
buildExpiredResult: vi.fn(),
},
transport: {
prepareTarget: vi.fn(),
deliverPending: vi.fn(),
},
observe: {
onDelivered,
},
});
const adapter = createLazyChannelApprovalNativeRuntimeAdapter({
isConfigured: vi.fn().mockReturnValue(true ),
shouldHandle: vi.fn().mockReturnValue(true ),
load,
});
adapter.observe?.onDelivered?.({ request: { id: "exec:1" } } as never);
expect(load).not.toHaveBeenCalled();
expect(onDelivered).not.toHaveBeenCalled();
await adapter.presentation.buildPendingPayload({
cfg: {} as never,
request: { id: "exec:1" } as never,
approvalKind: "exec" ,
nowMs: 1 ,
view: {} as never,
});
expect(load).toHaveBeenCalledTimes(1 );
adapter.observe?.onDelivered?.({ request: { id: "exec:1" } } as never);
expect(onDelivered).toHaveBeenCalledWith({ request: { id: "exec:1" } });
expect(load).toHaveBeenCalledTimes(1 );
});
});
Messung V0.5 in Prozent C=98 H=94 G=95
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland