From 109fd03a736fb25e614e2714b163f9f0170953f1 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 07:07:32 -0700 Subject: [PATCH 1/3] CL-6855: Wire host action ports through CHAT_STRINGS --- apps/web/src/approval-actions.ts | 9 +++++---- apps/web/src/block-response-actions.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/web/src/approval-actions.ts b/apps/web/src/approval-actions.ts index c92df0959..dc00d780b 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.blockApproveActionForbidden, + CHAT_STRINGS.blockApproveActionError, ); }, }; diff --git a/apps/web/src/block-response-actions.ts b/apps/web/src/block-response-actions.ts index 3677ae259..a65258a7d 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.blockApproveActionForbidden, }; } return { kind: "error", - message: cause instanceof Error ? cause.message : "Couldn't submit.", + message: + cause instanceof Error + ? cause.message + : CHAT_STRINGS.blockFormSubmitError, }; } } From ab12a7fc79cc531bf044e27c5f705dc63a8d4e69 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 09:43:20 -0700 Subject: [PATCH 2/3] Distinguish deny and form-response errors from approval copy --- apps/web/src/approval-actions.test.ts | 54 ++++++++++++++++++++ apps/web/src/approval-actions.ts | 4 +- apps/web/src/block-response-actions.test.ts | 55 +++++++++++++++++++++ apps/web/src/block-response-actions.ts | 2 +- packages/chat-ui/src/strings.test.ts | 26 ++++++++++ packages/chat-ui/src/strings.ts | 4 ++ 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/approval-actions.test.ts create mode 100644 apps/web/src/block-response-actions.test.ts diff --git a/apps/web/src/approval-actions.test.ts b/apps/web/src/approval-actions.test.ts new file mode 100644 index 000000000..146336838 --- /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 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 dc00d780b..7ac3583af 100644 --- a/apps/web/src/approval-actions.ts +++ b/apps/web/src/approval-actions.ts @@ -117,8 +117,8 @@ export function createChatApprovalActions( return resolve( () => rejectApproval(tenantId, approvalId), "rejected", - CHAT_STRINGS.blockApproveActionForbidden, - CHAT_STRINGS.blockApproveActionError, + 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..83cef6ef0 --- /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 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 a65258a7d..2e26ab82e 100644 --- a/apps/web/src/block-response-actions.ts +++ b/apps/web/src/block-response-actions.ts @@ -32,7 +32,7 @@ export function createChatBlockResponseActions( if (cause instanceof ChatApiError && cause.status === 403) { return { kind: "forbidden", - message: CHAT_STRINGS.blockApproveActionForbidden, + message: CHAT_STRINGS.blockFormSubmitForbidden, }; } return { 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…", From 7a3878ba6e7c49067465b8ffbc14293ec6d4c459 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 10:21:54 -0700 Subject: [PATCH 3/3] Cast fetch stubs through unknown for DOM lib types --- apps/web/src/approval-actions.test.ts | 2 +- apps/web/src/block-response-actions.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/approval-actions.test.ts b/apps/web/src/approval-actions.test.ts index 146336838..5724bed73 100644 --- a/apps/web/src/approval-actions.test.ts +++ b/apps/web/src/approval-actions.test.ts @@ -18,7 +18,7 @@ describe("createChatApprovalActions copy", () => { status, headers: { "content-type": "application/json" }, }), - )) as typeof fetch; + )) as unknown as typeof fetch; } function actions() { diff --git a/apps/web/src/block-response-actions.test.ts b/apps/web/src/block-response-actions.test.ts index 83cef6ef0..2cefcc917 100644 --- a/apps/web/src/block-response-actions.test.ts +++ b/apps/web/src/block-response-actions.test.ts @@ -17,7 +17,7 @@ describe("createChatBlockResponseActions copy", () => { status, headers: { "content-type": "application/json" }, }), - )) as typeof fetch; + )) as unknown as typeof fetch; } function actions() {