mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
fix(plugins): validate checkbox response input
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
1603a25a23
commit
942a2fe768
2 files changed
+41
-4
No files matched your search
@@ -1036,6 +1036,42 @@ describeEmbeddedPostgres("plugin orchestration APIs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("respondInteraction rejects duplicate checkbox option ids at the plugin boundary", async () => {
|
||||
const { companyId } = await seedCompanyAndAgent();
|
||||
const operatorUserId = randomUUID();
|
||||
await db.insert(companyMemberships).values({
|
||||
companyId, principalType: "user", principalId: operatorUserId, status: "active", membershipRole: "operator",
|
||||
});
|
||||
const issueId = randomUUID();
|
||||
await db.insert(issues).values({
|
||||
id: issueId, companyId, title: "Choose campaigns", status: "in_review", priority: "medium",
|
||||
});
|
||||
const interactionId = await seedInteraction(companyId, issueId, {
|
||||
kind: "request_checkbox_confirmation",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Which spikes should become campaigns?",
|
||||
options: [{ id: "spike-a", label: "Spike A" }],
|
||||
defaultSelectedOptionIds: [],
|
||||
minSelected: 0,
|
||||
maxSelected: 1,
|
||||
} as never,
|
||||
});
|
||||
const services = buildHostServices(db, "plugin-record-id", "paperclip.gateway", createEventBusStub());
|
||||
|
||||
await expect(services.issues.respondInteraction({
|
||||
issueId,
|
||||
interactionId,
|
||||
companyId,
|
||||
action: "accept",
|
||||
actorUserId: operatorUserId,
|
||||
selectedOptionIds: ["spike-a", "spike-a"],
|
||||
})).rejects.toThrow("selectedOptionIds must be unique");
|
||||
|
||||
const [row] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, interactionId));
|
||||
expect(row?.status).toBe("pending");
|
||||
});
|
||||
|
||||
it("respondInteraction converges (applied:false) when the interaction is already resolved", async () => {
|
||||
const { companyId, agentId } = await seedCompanyAndAgent();
|
||||
const humanUserId = randomUUID();
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
PluginExecutionWorkspaceMetadata,
|
||||
} from "@paperclipai/plugin-sdk";
|
||||
import type { CreateIssueThreadInteraction, InviteJoinType, IssueDocumentSummary, PermissionKey, PrincipalType } from "@paperclipai/shared";
|
||||
import { pluginOperationIssueOriginKind } from "@paperclipai/shared";
|
||||
import { acceptIssueThreadInteractionSchema, pluginOperationIssueOriginKind } from "@paperclipai/shared";
|
||||
import { companyService } from "./companies.js";
|
||||
import { agentService } from "./agents.js";
|
||||
import { projectService } from "./projects.js";
|
||||
@@ -2286,12 +2286,13 @@ export function buildHostServices(
|
||||
status: issue.status,
|
||||
};
|
||||
if (params.action === "accept") {
|
||||
const acceptInput = acceptIssueThreadInteractionSchema.parse({
|
||||
selectedOptionIds: params.selectedOptionIds,
|
||||
});
|
||||
const result = await interactions.acceptInteraction(
|
||||
{ id: issue.id, companyId, projectId: issue.projectId ?? null, goalId: issue.goalId ?? null },
|
||||
params.interactionId,
|
||||
params.selectedOptionIds === undefined
|
||||
? {}
|
||||
: { selectedOptionIds: params.selectedOptionIds },
|
||||
acceptInput,
|
||||
actor,
|
||||
);
|
||||
resolved = result.interaction as typeof current;
|
||||
|
||||
Reference in new issue
Block a user