refactor: share plugin approval test helpers

This commit is contained in:
Vincent Koc
2026-06-01 02:39:24 +02:00
parent 6a96058f50
commit b475de834a
+209 -251
View File
@@ -8,6 +8,50 @@ function createManager() {
return new ExecApprovalManager<PluginApprovalRequestPayload>();
}
function createLogGatewayMock() {
return { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() };
}
function createApprovalContext(
params: {
broadcast?: ReturnType<typeof vi.fn>;
hasExecApprovalClients?: GatewayRequestHandlerOptions["context"]["hasExecApprovalClients"];
} = {},
): GatewayRequestHandlerOptions["context"] {
return {
broadcast: params.broadcast ?? vi.fn(),
logGateway: createLogGatewayMock(),
hasExecApprovalClients: params.hasExecApprovalClients ?? (() => true),
} as unknown as GatewayRequestHandlerOptions["context"];
}
function createClient(
params: {
connId?: string;
clientId?: string;
displayName?: string;
deviceId?: string;
scopes?: string[];
} = {},
): GatewayRequestHandlerOptions["client"] {
const connect: Record<string, unknown> = {
client: {
id: params.clientId ?? "test-client",
displayName: params.displayName ?? "Test Client",
},
};
if (params.deviceId) {
connect.device = { id: params.deviceId };
}
if (params.scopes) {
connect.scopes = params.scopes;
}
return {
connId: params.connId ?? "conn-test-client",
connect,
} as unknown as GatewayRequestHandlerOptions["client"];
}
function createMockOptions(
method: string,
params: Record<string, unknown>,
@@ -16,29 +60,16 @@ function createMockOptions(
return {
req: { method, params, id: "req-1" },
params,
client: {
connId: "conn-test-client",
connect: {
client: { id: "test-client", displayName: "Test Client" },
},
},
client: createClient(),
isWebchatConnect: () => false,
respond: vi.fn(),
context: {
broadcast: vi.fn(),
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
hasExecApprovalClients: () => true,
},
context: createApprovalContext(),
...overrides,
} as unknown as GatewayRequestHandlerOptions;
}
function createNoExecApprovalContext(): GatewayRequestHandlerOptions["context"] {
return {
broadcast: vi.fn(),
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
hasExecApprovalClients: () => false,
} as unknown as GatewayRequestHandlerOptions["context"];
return createApprovalContext({ hasExecApprovalClients: () => false });
}
type MockCallSource = {
@@ -59,15 +90,15 @@ function requireArray(value: unknown, label: string): unknown[] {
return value as unknown[];
}
function mockCall(source: MockCallSource, index: number, label: string) {
const call = source.mock.calls[index];
function mockCall(source: unknown, index: number, label: string) {
const call = (source as MockCallSource).mock.calls[index];
if (!call) {
throw new Error(`Expected ${label}`);
}
return call;
}
function responseCall(source: MockCallSource, index = 0) {
function responseCall(source: unknown, index = 0) {
const call = mockCall(source, index, `response call ${index}`);
return {
ok: call[0],
@@ -76,16 +107,17 @@ function responseCall(source: MockCallSource, index = 0) {
};
}
function responseResult(source: MockCallSource, index = 0) {
function responseResult(source: unknown, index = 0) {
return requireRecord(responseCall(source, index).result, `response result ${index}`);
}
function responseError(source: MockCallSource, index = 0) {
function responseError(source: unknown, index = 0) {
return requireRecord(responseCall(source, index).error, `response error ${index}`);
}
function acceptedResult(source: MockCallSource) {
const call = Array.from(source.mock.calls).find((candidate) => {
function acceptedResult(source: unknown) {
const callSource = source as MockCallSource;
const call = Array.from(callSource.mock.calls).find((candidate) => {
const result = candidate[1];
return typeof result === "object" && result !== null && "status" in result
? (result as Record<string, unknown>).status === "accepted"
@@ -97,12 +129,74 @@ function acceptedResult(source: MockCallSource) {
return requireRecord(call[1], "accepted response result");
}
function acceptedApprovalId(source: MockCallSource) {
function acceptedApprovalId(source: unknown) {
const id = acceptedResult(source).id;
expect(id, "accepted approval id").toBeTypeOf("string");
return id as string;
}
function expectResponseOk(source: unknown, index = 0) {
const call = responseCall(source, index);
expect(call.ok).toBe(true);
expect(call.error).toBeUndefined();
return requireRecord(call.result, `response result ${index}`);
}
function expectResponseRejected(source: unknown, index = 0) {
expect(responseCall(source, index).ok).toBe(false);
return responseError(source, index);
}
async function waitForAcceptedApproval(respond: unknown) {
await vi.waitFor(() => {
const accepted = acceptedResult(respond);
expect(accepted.status).toBe("accepted");
expect(accepted.id).toBeTypeOf("string");
});
return acceptedApprovalId(respond);
}
function createOwnedClient(owner: "owner" | "other" = "owner") {
return createClient({
connId: `conn-${owner}`,
clientId: `client-${owner}`,
deviceId: `device-${owner}`,
});
}
function registerApproval(
approvalManager: ExecApprovalManager<PluginApprovalRequestPayload>,
params: {
title?: string;
description?: string;
id?: string;
allowedDecisions?: PluginApprovalRequestPayload["allowedDecisions"];
} = {},
) {
const request = {
title: params.title ?? "T",
description: params.description ?? "D",
...(params.allowedDecisions ? { allowedDecisions: params.allowedDecisions } : {}),
};
const record = params.id
? approvalManager.create(request, 60_000, params.id)
: approvalManager.create(request, 60_000);
void approvalManager.register(record, 60_000);
return record;
}
function registerOwnedApproval(
approvalManager: ExecApprovalManager<PluginApprovalRequestPayload>,
params: { title: string; id?: string; owner?: "owner" | "other" },
) {
const record = registerApproval(approvalManager, { title: params.title, id: params.id });
const owner = params.owner ?? "owner";
record.requestedByDeviceId = `device-${owner}`;
record.requestedByConnId = `conn-${owner}`;
record.requestedByClientId = `client-${owner}`;
return record;
}
function expectPluginApprovalId(value: unknown, label: string): string {
expect(value, label).toBeTypeOf("string");
if (typeof value !== "string") {
@@ -120,11 +214,7 @@ function expectPluginApprovalId(value: unknown, label: string): string {
}
function broadcastCall(opts: GatewayRequestHandlerOptions, index = 0) {
const call = mockCall(
opts.context.broadcast as unknown as MockCallSource,
index,
"broadcast call",
);
const call = mockCall(opts.context.broadcast, index, "broadcast call");
return {
event: call?.[0],
payload: requireRecord(call?.[1], "broadcast payload"),
@@ -137,6 +227,25 @@ const invalidParamMethodCases = [
{ method: "plugin.approval.resolve" },
] as const;
const invalidRequestCases = [
{
name: "invalid severity value",
params: { title: "T", description: "D", severity: "extreme" },
},
{
name: "title exceeding max length",
params: { title: "x".repeat(81), description: "D" },
},
{
name: "description exceeding max length",
params: { title: "T", description: "x".repeat(257) },
},
{
name: "timeoutMs exceeding max",
params: { title: "T", description: "D", timeoutMs: 700_000 },
},
] as const;
describe("createPluginApprovalHandlers", () => {
let manager: ExecApprovalManager<PluginApprovalRequestPayload>;
@@ -163,9 +272,8 @@ describe("createPluginApprovalHandlers", () => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions(method, {});
await handlers[method](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseCall(opts.respond as unknown as MockCallSource).result).toBeUndefined();
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
expect(responseCall(opts.respond).result).toBeUndefined();
expect(expectResponseRejected(opts.respond).code).toBeTypeOf("string");
});
});
@@ -188,12 +296,7 @@ describe("createPluginApprovalHandlers", () => {
// Instead, let it run and resolve the approval after the accepted response.
const handlerPromise = handlers["plugin.approval.request"](opts);
// Wait for the twoPhase "accepted" response
await vi.waitFor(() => {
const accepted = acceptedResult(respond as unknown as MockCallSource);
expect(accepted.status).toBe("accepted");
expect(accepted.id).toBeTypeOf("string");
});
const approvalId = await waitForAcceptedApproval(respond);
const requestedBroadcast = broadcastCall(opts);
expect(requestedBroadcast.event).toBe("plugin.approval.requested");
@@ -201,18 +304,15 @@ describe("createPluginApprovalHandlers", () => {
expect(requestedBroadcast.options).toEqual({ dropIfSlow: true });
// Resolve the approval so the handler can complete
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
expect(manager.getSnapshot(approvalId)?.requestedByClientId).toBe("test-client");
manager.resolve(approvalId, "allow-once");
await handlerPromise;
// Final response with decision
const finalResult = responseResult(respond as unknown as MockCallSource, 1);
expect(responseCall(respond as unknown as MockCallSource, 1).ok).toBe(true);
const finalResult = expectResponseOk(respond, 1);
expect(finalResult.id).toBe(approvalId);
expect(finalResult.decision).toBe("allow-once");
expect(responseCall(respond as unknown as MockCallSource, 1).error).toBeUndefined();
});
it("expires immediately when no approval route", async () => {
@@ -228,9 +328,7 @@ describe("createPluginApprovalHandlers", () => {
},
);
await handlers["plugin.approval.request"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(true);
expect(responseResult(opts.respond as unknown as MockCallSource).decision).toBeNull();
expect(responseCall(opts.respond as unknown as MockCallSource).error).toBeUndefined();
expect(expectResponseOk(opts.respond).decision).toBeNull();
});
it("passes caller connId to hasExecApprovalClients to exclude self", async () => {
@@ -240,15 +338,12 @@ describe("createPluginApprovalHandlers", () => {
"plugin.approval.request",
{ title: "T", description: "D" },
{
client: {
client: createClient({
connId: "backend-conn-42",
connect: { client: { id: "test", displayName: "Test" } },
} as unknown as GatewayRequestHandlerOptions["client"],
context: {
broadcast: vi.fn(),
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
hasExecApprovalClients,
} as unknown as GatewayRequestHandlerOptions["context"],
clientId: "test",
displayName: "Test",
}),
context: createApprovalContext({ hasExecApprovalClients }),
},
);
await handlers["plugin.approval.request"](opts);
@@ -271,23 +366,12 @@ describe("createPluginApprovalHandlers", () => {
},
{
respond,
context: {
broadcast: vi.fn(),
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
hasExecApprovalClients: () => false,
} as unknown as GatewayRequestHandlerOptions["context"],
context: createApprovalContext({ hasExecApprovalClients: () => false }),
},
);
const requestPromise = handlers["plugin.approval.request"](opts);
await vi.waitFor(() => {
const accepted = acceptedResult(respond as unknown as MockCallSource);
expect(accepted.status).toBe("accepted");
expect(accepted.id).toBeTypeOf("string");
});
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
const approvalId = await waitForAcceptedApproval(respond);
manager.resolve(approvalId, "allow-once");
await requestPromise;
@@ -296,50 +380,11 @@ describe("createPluginApprovalHandlers", () => {
}
});
it("rejects invalid severity value", async () => {
it.each(invalidRequestCases)("rejects $name", async ({ params }) => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions("plugin.approval.request", {
title: "T",
description: "D",
severity: "extreme",
});
const opts = createMockOptions("plugin.approval.request", params);
await handlers["plugin.approval.request"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
});
it("rejects title exceeding max length", async () => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions("plugin.approval.request", {
title: "x".repeat(81),
description: "D",
});
await handlers["plugin.approval.request"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
});
it("rejects description exceeding max length", async () => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions("plugin.approval.request", {
title: "T",
description: "x".repeat(257),
});
await handlers["plugin.approval.request"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
});
it("rejects timeoutMs exceeding max", async () => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions("plugin.approval.request", {
title: "T",
description: "D",
timeoutMs: 700_000,
});
await handlers["plugin.approval.request"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string");
expect(expectResponseRejected(opts.respond).code).toBeTypeOf("string");
});
it("generates plugin-prefixed IDs", async () => {
@@ -350,15 +395,11 @@ describe("createPluginApprovalHandlers", () => {
{ title: "T", description: "D" },
{
respond,
context: {
broadcast: vi.fn(),
logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
hasExecApprovalClients: () => false,
} as unknown as GatewayRequestHandlerOptions["context"],
context: createApprovalContext({ hasExecApprovalClients: () => false }),
},
);
await handlers["plugin.approval.request"](opts);
const result = responseResult(respond as unknown as MockCallSource);
const result = responseResult(respond);
expectPluginApprovalId(result?.id, "generated plugin approval id");
});
@@ -390,10 +431,8 @@ describe("createPluginApprovalHandlers", () => {
description: "D",
});
await handlers["plugin.approval.request"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
"unexpected property",
);
expect(responseCall(opts.respond).ok).toBe(false);
expect(responseError(opts.respond).message).toContain("unexpected property");
});
it("stores scoped allowed decisions on plugin approval requests", async () => {
@@ -411,13 +450,7 @@ describe("createPluginApprovalHandlers", () => {
);
const handlerPromise = handlers["plugin.approval.request"](opts);
await vi.waitFor(() => {
const accepted = acceptedResult(respond as unknown as MockCallSource);
expect(accepted.status).toBe("accepted");
expect(accepted.id).toBeTypeOf("string");
});
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
const approvalId = await waitForAcceptedApproval(respond);
expect(manager.getSnapshot(approvalId)?.request.allowedDecisions).toEqual([
"allow-once",
"deny",
@@ -442,30 +475,23 @@ describe("createPluginApprovalHandlers", () => {
);
const handlerPromise = handlers["plugin.approval.request"](requestOpts);
await vi.waitFor(() => {
const accepted = acceptedResult(respond as unknown as MockCallSource);
expect(accepted.status).toBe("accepted");
expect(accepted.id).toBeTypeOf("string");
});
const approvalId = await waitForAcceptedApproval(respond);
const listRespond = vi.fn();
await handlers["plugin.approval.list"](
createMockOptions("plugin.approval.list", {}, { respond: listRespond }),
);
expect(responseCall(listRespond as unknown as MockCallSource).ok).toBe(true);
const approvals = requireArray(
responseCall(listRespond as unknown as MockCallSource).result,
"approval list",
);
const listCall = responseCall(listRespond);
expect(listCall.ok).toBe(true);
expect(listCall.error).toBeUndefined();
const approvals = requireArray(listCall.result, "approval list");
expect(approvals).toHaveLength(1);
const approval = requireRecord(approvals[0], "approval");
const listedApprovalId = expectPluginApprovalId(approval.id, "listed approval id");
const request = requireRecord(approval.request, "approval request");
expect(request.title).toBe("Sensitive action");
expect(request.description).toBe("Desc");
expect(responseCall(listRespond as unknown as MockCallSource).error).toBeUndefined();
const approvalId = acceptedApprovalId(respond as unknown as MockCallSource);
expect(listedApprovalId).toBe(approvalId);
manager.resolve(approvalId, "allow-once");
await handlerPromise;
@@ -473,21 +499,12 @@ describe("createPluginApprovalHandlers", () => {
it("lists only plugin approvals owned by the caller", async () => {
const handlers = createPluginApprovalHandlers(manager);
const visible = manager.create(
{ title: "Visible", description: "D" },
60_000,
"plugin:visible",
);
visible.requestedByDeviceId = "device-owner";
visible.requestedByConnId = "conn-owner";
visible.requestedByClientId = "client-owner";
void manager.register(visible, 60_000);
const hidden = manager.create({ title: "Hidden", description: "D" }, 60_000, "plugin:hidden");
hidden.requestedByDeviceId = "device-other";
hidden.requestedByConnId = "conn-other";
hidden.requestedByClientId = "client-other";
void manager.register(hidden, 60_000);
registerOwnedApproval(manager, { title: "Visible", id: "plugin:visible" });
registerOwnedApproval(manager, {
title: "Hidden",
id: "plugin:hidden",
owner: "other",
});
const listRespond = vi.fn();
await handlers["plugin.approval.list"](
@@ -496,22 +513,14 @@ describe("createPluginApprovalHandlers", () => {
{},
{
respond: listRespond,
client: {
connId: "conn-owner",
connect: {
client: { id: "client-owner" },
device: { id: "device-owner" },
},
} as unknown as GatewayRequestHandlerOptions["client"],
client: createOwnedClient(),
},
),
);
expect(responseCall(listRespond as unknown as MockCallSource).ok).toBe(true);
const approvals = requireArray(
responseCall(listRespond as unknown as MockCallSource).result,
"approval list",
);
const listCall = responseCall(listRespond);
expect(listCall.ok).toBe(true);
const approvals = requireArray(listCall.result, "approval list");
expect(approvals.map((entry) => requireRecord(entry, "approval").id)).toEqual([
"plugin:visible",
]);
@@ -523,89 +532,67 @@ describe("createPluginApprovalHandlers", () => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions("plugin.approval.waitDecision", {});
await handlers["plugin.approval.waitDecision"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
"id is required",
);
expect(expectResponseRejected(opts.respond).message).toContain("id is required");
});
it("returns not found for unknown id", async () => {
const handlers = createPluginApprovalHandlers(manager);
const opts = createMockOptions("plugin.approval.waitDecision", { id: "unknown" });
await handlers["plugin.approval.waitDecision"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
"expired or not found",
);
expect(expectResponseRejected(opts.respond).message).toContain("expired or not found");
});
it("returns not found for approvals hidden from the caller", async () => {
const handlers = createPluginApprovalHandlers(manager);
const record = manager.create({ title: "T", description: "D" }, 60_000);
record.requestedByDeviceId = "device-owner";
record.requestedByConnId = "conn-owner";
record.requestedByClientId = "client-owner";
void manager.register(record, 60_000);
const record = registerOwnedApproval(manager, { title: "T" });
manager.resolve(record.id, "allow-once");
const opts = createMockOptions(
"plugin.approval.waitDecision",
{ id: record.id },
{
client: {
client: createClient({
connId: "conn-other",
connect: {
client: { id: "client-other" },
device: { id: "device-other" },
scopes: ["operator.approvals"],
},
} as unknown as GatewayRequestHandlerOptions["client"],
clientId: "client-other",
deviceId: "device-other",
scopes: ["operator.approvals"],
}),
},
);
await handlers["plugin.approval.waitDecision"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).message).toContain(
"expired or not found",
);
expect(expectResponseRejected(opts.respond).message).toContain("expired or not found");
});
it("returns decision when resolved", async () => {
const handlers = createPluginApprovalHandlers(manager);
const record = manager.create({ title: "T", description: "D" }, 60_000);
void manager.register(record, 60_000);
const record = registerApproval(manager);
// Resolve before waiting
manager.resolve(record.id, "allow-once");
const opts = createMockOptions("plugin.approval.waitDecision", { id: record.id });
await handlers["plugin.approval.waitDecision"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(true);
expect(responseResult(opts.respond as unknown as MockCallSource).id).toBe(record.id);
expect(responseResult(opts.respond as unknown as MockCallSource).decision).toBe("allow-once");
expect(responseCall(opts.respond as unknown as MockCallSource).error).toBeUndefined();
const result = expectResponseOk(opts.respond);
expect(result.id).toBe(record.id);
expect(result.decision).toBe("allow-once");
});
});
describe("plugin.approval.resolve", () => {
it("rejects invalid decision", async () => {
const handlers = createPluginApprovalHandlers(manager);
const record = manager.create({ title: "T", description: "D" }, 60_000);
void manager.register(record, 60_000);
const record = registerApproval(manager);
const opts = createMockOptions("plugin.approval.resolve", {
id: record.id,
decision: "invalid",
});
await handlers["plugin.approval.resolve"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
expect(responseError(opts.respond as unknown as MockCallSource).message).toBe(
"invalid decision",
);
expect(expectResponseRejected(opts.respond).message).toBe("invalid decision");
});
it("resolves a pending approval", async () => {
const handlers = createPluginApprovalHandlers(manager);
const record = manager.create({ title: "T", description: "D" }, 60_000);
void manager.register(record, 60_000);
const record = registerApproval(manager);
const opts = createMockOptions("plugin.approval.resolve", {
id: record.id,
@@ -622,33 +609,17 @@ describe("createPluginApprovalHandlers", () => {
it("resolves only plugin approvals owned by the caller", async () => {
const handlers = createPluginApprovalHandlers(manager);
const visible = manager.create(
{ title: "Visible", description: "D" },
60_000,
"plugin:abcd-visible",
);
visible.requestedByDeviceId = "device-owner";
visible.requestedByConnId = "conn-owner";
visible.requestedByClientId = "client-owner";
void manager.register(visible, 60_000);
const visible = registerOwnedApproval(manager, {
title: "Visible",
id: "plugin:abcd-visible",
});
const hidden = registerOwnedApproval(manager, {
title: "Hidden",
id: "plugin:abcd-hidden",
owner: "other",
});
const hidden = manager.create(
{ title: "Hidden", description: "D" },
60_000,
"plugin:abcd-hidden",
);
hidden.requestedByDeviceId = "device-other";
hidden.requestedByConnId = "conn-other";
hidden.requestedByClientId = "client-other";
void manager.register(hidden, 60_000);
const ownerClient = {
connId: "conn-owner",
connect: {
client: { id: "client-owner" },
device: { id: "device-owner" },
},
} as unknown as GatewayRequestHandlerOptions["client"];
const ownerClient = createOwnedClient();
const resolveRespond = vi.fn();
await handlers["plugin.approval.resolve"](
createMockOptions(
@@ -681,8 +652,7 @@ describe("createPluginApprovalHandlers", () => {
},
),
);
expect(responseCall(hiddenRespond as unknown as MockCallSource).ok).toBe(false);
const error = responseError(hiddenRespond as unknown as MockCallSource);
const error = expectResponseRejected(hiddenRespond);
expect(error.code).toBe("INVALID_REQUEST");
expect(error.message).toBe("unknown or expired approval id");
expect(manager.getSnapshot(hidden.id)?.decision).toBeUndefined();
@@ -690,23 +660,16 @@ describe("createPluginApprovalHandlers", () => {
it("rejects decisions outside plugin approval allowed decisions", async () => {
const handlers = createPluginApprovalHandlers(manager);
const record = manager.create(
{
title: "T",
description: "D",
allowedDecisions: ["allow-once", "deny"],
},
60_000,
);
void manager.register(record, 60_000);
const record = registerApproval(manager, {
allowedDecisions: ["allow-once", "deny"],
});
const opts = createMockOptions("plugin.approval.resolve", {
id: record.id,
decision: "allow-always",
});
await handlers["plugin.approval.resolve"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
const error = responseError(opts.respond as unknown as MockCallSource);
const error = expectResponseRejected(opts.respond);
expect(error.code).toBe("INVALID_REQUEST");
expect(error.message).toBe("allow-always is unavailable for this plugin approval");
expect(error.details).toEqual({ allowedDecisions: ["allow-once", "deny"] });
@@ -720,8 +683,7 @@ describe("createPluginApprovalHandlers", () => {
decision: "allow-once",
});
await handlers["plugin.approval.resolve"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
const error = responseError(opts.respond as unknown as MockCallSource);
const error = expectResponseRejected(opts.respond);
expect(error.code).toBe("INVALID_REQUEST");
expect(error.message).toContain("unknown or expired");
expect(requireRecord(error.details, "error details").reason).toBe("APPROVAL_NOT_FOUND");
@@ -729,8 +691,7 @@ describe("createPluginApprovalHandlers", () => {
it("accepts unique short id prefixes", async () => {
const handlers = createPluginApprovalHandlers(manager);
const record = manager.create({ title: "T", description: "D" }, 60_000, "abcdef-1234");
void manager.register(record, 60_000);
const record = registerApproval(manager, { id: "abcdef-1234" });
const opts = createMockOptions("plugin.approval.resolve", {
id: "abcdef",
@@ -743,18 +704,15 @@ describe("createPluginApprovalHandlers", () => {
it("does not leak candidate ids when prefixes are ambiguous", async () => {
const handlers = createPluginApprovalHandlers(manager);
const recordA = manager.create({ title: "A", description: "D" }, 60_000, "plugin:abc-1111");
const recordB = manager.create({ title: "B", description: "D" }, 60_000, "plugin:abc-2222");
void manager.register(recordA, 60_000);
void manager.register(recordB, 60_000);
registerApproval(manager, { title: "A", id: "plugin:abc-1111" });
registerApproval(manager, { title: "B", id: "plugin:abc-2222" });
const opts = createMockOptions("plugin.approval.resolve", {
id: "plugin:abc",
decision: "deny",
});
await handlers["plugin.approval.resolve"](opts);
expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false);
const error = responseError(opts.respond as unknown as MockCallSource);
const error = expectResponseRejected(opts.respond);
expect(error.code).toBe("INVALID_REQUEST");
expect(error.message).toBe("unknown or expired approval id");
});