diff --git a/apps/web/src/approval-actions.test.ts b/apps/web/src/approval-actions.test.ts new file mode 100644 index 000000000..5724bed73 --- /dev/null +++ b/apps/web/src/approval-actions.test.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { QueryClient } from "@tanstack/react-query"; +import { CHAT_STRINGS } from "@corbits/chat-ui"; + +import { createChatApprovalActions } from "./approval-actions"; + +describe("createChatApprovalActions copy", () => { + const realFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = realFetch; + }); + + function stubStatus(status: number): void { + globalThis.fetch = (() => + Promise.resolve( + new Response(JSON.stringify({ message: "nope" }), { + status, + headers: { "content-type": "application/json" }, + }), + )) as unknown as typeof fetch; + } + + function actions() { + return createChatApprovalActions( + "tenant-1", + new QueryClient({ defaultOptions: { queries: { retry: false } } }), + ); + } + + test("approve 403 uses approve forbidden copy, not deny", async () => { + stubStatus(403); + const result = await actions().approve("appr_1"); + expect(result).toEqual({ + kind: "forbidden", + message: CHAT_STRINGS.blockApproveActionForbidden, + }); + expect(CHAT_STRINGS.blockApproveActionForbidden).not.toBe( + CHAT_STRINGS.blockDenyActionForbidden, + ); + }); + + test("reject 403 uses deny forbidden copy, not approve", async () => { + stubStatus(403); + const result = await actions().reject("appr_1"); + expect(result).toEqual({ + kind: "forbidden", + message: CHAT_STRINGS.blockDenyActionForbidden, + }); + expect(CHAT_STRINGS.blockDenyActionForbidden).not.toBe( + CHAT_STRINGS.blockApproveActionForbidden, + ); + }); +}); diff --git a/apps/web/src/approval-actions.ts b/apps/web/src/approval-actions.ts index c92df0959..7ac3583af 100644 --- a/apps/web/src/approval-actions.ts +++ b/apps/web/src/approval-actions.ts @@ -9,6 +9,7 @@ import type { QueryClient } from "@tanstack/react-query"; import type { ApprovalActions, ApprovalDecisionResult } from "@corbits/chat-ui"; +import { CHAT_STRINGS } from "@corbits/chat-ui"; import { ApiQueryError } from "@corbits/api-query"; import { approveApproval, getApprovalNeedsYou, rejectApproval } from "./api"; @@ -108,16 +109,16 @@ export function createChatApprovalActions( return resolve( () => approveApproval(tenantId, approvalId), "approved", - "You do not have permission to approve this.", - "Couldn't approve this request.", + CHAT_STRINGS.blockApproveActionForbidden, + CHAT_STRINGS.blockApproveActionError, ); }, reject(approvalId) { return resolve( () => rejectApproval(tenantId, approvalId), "rejected", - "You do not have permission to deny this.", - "Couldn't deny this request.", + CHAT_STRINGS.blockDenyActionForbidden, + CHAT_STRINGS.blockDenyActionError, ); }, }; diff --git a/apps/web/src/block-response-actions.test.ts b/apps/web/src/block-response-actions.test.ts new file mode 100644 index 000000000..2cefcc917 --- /dev/null +++ b/apps/web/src/block-response-actions.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { CHAT_STRINGS } from "@corbits/chat-ui"; + +import { createChatBlockResponseActions } from "./block-response-actions"; + +describe("createChatBlockResponseActions copy", () => { + const realFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = realFetch; + }); + + function stubStatus(status: number): void { + globalThis.fetch = (() => + Promise.resolve( + new Response(JSON.stringify({ message: "nope" }), { + status, + headers: { "content-type": "application/json" }, + }), + )) as unknown as typeof fetch; + } + + function actions() { + return createChatBlockResponseActions("tenant-1", "wb_1"); + } + + test("form 403 uses form-submit forbidden, not approve copy", async () => { + stubStatus(403); + const result = await actions().submitForm("msg_1", "form_1", { + name: "Ada", + }); + expect(result).toEqual({ + kind: "forbidden", + message: CHAT_STRINGS.blockFormSubmitForbidden, + }); + expect(CHAT_STRINGS.blockFormSubmitForbidden).not.toBe( + CHAT_STRINGS.blockApproveActionForbidden, + ); + }); + + test("poll and question 403 share form-submit forbidden copy", async () => { + stubStatus(403); + const ports = actions(); + const poll = await ports.submitPoll("msg_1", "poll_1", ["choice_1"]); + const question = await ports.submitQuestion("msg_1", "q_1", "yes", 0); + expect(poll).toEqual({ + kind: "forbidden", + message: CHAT_STRINGS.blockFormSubmitForbidden, + }); + expect(question).toEqual({ + kind: "forbidden", + message: CHAT_STRINGS.blockFormSubmitForbidden, + }); + }); +}); diff --git a/apps/web/src/block-response-actions.ts b/apps/web/src/block-response-actions.ts index 3677ae259..2e26ab82e 100644 --- a/apps/web/src/block-response-actions.ts +++ b/apps/web/src/block-response-actions.ts @@ -10,6 +10,7 @@ import type { BlockResponseSubmitResult, } from "@corbits/chat-ui"; import { + CHAT_STRINGS, ChatApiError, getBlockResponses, submitFormResponse, @@ -31,13 +32,15 @@ export function createChatBlockResponseActions( if (cause instanceof ChatApiError && cause.status === 403) { return { kind: "forbidden", - message: - "You do not have permission to respond in this conversation.", + message: CHAT_STRINGS.blockFormSubmitForbidden, }; } return { kind: "error", - message: cause instanceof Error ? cause.message : "Couldn't submit.", + message: + cause instanceof Error + ? cause.message + : CHAT_STRINGS.blockFormSubmitError, }; } } diff --git a/packages/chat-ui/src/strings.test.ts b/packages/chat-ui/src/strings.test.ts index f31ac73a3..ea2938039 100644 --- a/packages/chat-ui/src/strings.test.ts +++ b/packages/chat-ui/src/strings.test.ts @@ -49,3 +49,29 @@ describe("agentsTyping copy", () => { expect(CHAT_STRINGS.agentsTyping([])).toBe(""); }); }); + +describe("approve vs deny vs form action copy", () => { + test("deny forbidden and error are distinct from approve copy", () => { + expect(CHAT_STRINGS.blockDenyActionForbidden).toBe( + "You do not have permission to deny this.", + ); + expect(CHAT_STRINGS.blockDenyActionError).toBe( + "Couldn't deny this request.", + ); + expect(CHAT_STRINGS.blockDenyActionForbidden).not.toBe( + CHAT_STRINGS.blockApproveActionForbidden, + ); + expect(CHAT_STRINGS.blockDenyActionError).not.toBe( + CHAT_STRINGS.blockApproveActionError, + ); + }); + + test("form submit forbidden is respond copy, not approve copy", () => { + expect(CHAT_STRINGS.blockFormSubmitForbidden).toBe( + "You do not have permission to respond in this conversation.", + ); + expect(CHAT_STRINGS.blockFormSubmitForbidden).not.toBe( + CHAT_STRINGS.blockApproveActionForbidden, + ); + }); +}); diff --git a/packages/chat-ui/src/strings.ts b/packages/chat-ui/src/strings.ts index 4b6c2f845..942339a73 100644 --- a/packages/chat-ui/src/strings.ts +++ b/packages/chat-ui/src/strings.ts @@ -170,6 +170,8 @@ export const CHAT_STRINGS = { blockApproveRejecting: "Denying…", blockApproveActionForbidden: "You do not have permission to act on this.", blockApproveActionError: "Couldn't reach the approval — try again.", + blockDenyActionForbidden: "You do not have permission to deny this.", + blockDenyActionError: "Couldn't deny this request.", blockApprovePlatformRequestedBy: (agentName: string) => `${agentName} is asking to`, blockApproveAgentNoteLabel: "Agent's note", @@ -184,6 +186,8 @@ export const CHAT_STRINGS = { blockFormSubmitted: "Submitted", blockFormEdit: "Edit response", blockFormSubmitError: "Couldn't submit — try again.", + blockFormSubmitForbidden: + "You do not have permission to respond in this conversation.", blockFormFieldRequired: "This field is required.", blockQuestionFreeTextLabel: "Type your own answer", blockQuestionFreeTextPlaceholder: "Type your own answer…",