Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions apps/builder/__tests__/coexist-messenger-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("coexist APIs", () => {
mockDisable.mockResolvedValue({ success: true })
})

test("Messenger enabled:true delegates to coexistService.enable", async () => {
test("Messenger enabled:true delegates to coexistService.enable with aiReadsSyncedHistory defaulted to false", async () => {
const result = await call(
integrationMessengerCoexistAPIs.setCoexistMessengerAPI,
{ workspaceId: "ws-1", integrationId: "int-1", enabled: true },
Expand All @@ -86,10 +86,32 @@ describe("coexist APIs", () => {
workspaceId: "ws-1",
integrationId: "int-1",
channel: "messenger",
aiReadsSyncedHistory: false,
})
expect(mockDisable).not.toHaveBeenCalled()
})

test("Messenger enabled:true, aiReadsSyncedHistory:true delegates it through to coexistService.enable", async () => {
const result = await call(
integrationMessengerCoexistAPIs.setCoexistMessengerAPI,
{
workspaceId: "ws-1",
integrationId: "int-1",
enabled: true,
aiReadsSyncedHistory: true,
},
{ context: stubContext },
)

expect(result).toEqual({ success: true })
expect(mockEnable).toHaveBeenCalledWith({
workspaceId: "ws-1",
integrationId: "int-1",
channel: "messenger",
aiReadsSyncedHistory: true,
})
})

test("Messenger enabled:false delegates to coexistService.disable", async () => {
const result = await call(
integrationMessengerCoexistAPIs.setCoexistMessengerAPI,
Expand All @@ -105,7 +127,7 @@ describe("coexist APIs", () => {
})
})

test("Instagram endpoint delegates with channel instagram", async () => {
test("Instagram endpoint delegates with channel instagram and aiReadsSyncedHistory defaulted to false", async () => {
const result = await call(
integrationInstagramCoexistAPIs.setCoexistInstagramAPI,
{ workspaceId: "ws-1", integrationId: "ig-1", enabled: true },
Expand All @@ -117,6 +139,7 @@ describe("coexist APIs", () => {
workspaceId: "ws-1",
integrationId: "ig-1",
channel: "instagram",
aiReadsSyncedHistory: false,
})
})

Expand Down
182 changes: 182 additions & 0 deletions apps/builder/__tests__/coexist-popup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import type { ReactNode } from "react"
import { act } from "react"
import { createRoot, type Root } from "react-dom/client"
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

const { mockKyPost } = vi.hoisted(() => ({
mockKyPost: vi.fn(),
}))

/** Echoes the key back so assertions never depend on the English copy. */
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}))

vi.mock("ky", () => ({
default: { post: mockKyPost },
}))

vi.mock("sonner", () => ({
toast: { error: vi.fn(), success: vi.fn() },
}))

vi.mock("@/lib/errors/client-handler", () => ({
clientErrorHandler: vi.fn(),
}))

vi.mock("@chatbotx.io/ui/components/ui/dialog", () => ({
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogDescription: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogFooter: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogHeader: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
}))

// jsdom ships no ResizeObserver, and Radix measures the switch thumb through it.
Object.assign(globalThis, {
ResizeObserver: class {
observe = vi.fn()
unobserve = vi.fn()
disconnect = vi.fn()
},
})

// jsdom ships no PointerEvent constructor; the Switch's click handler
// re-dispatches one to drive its underlying <input type="checkbox">.
if (typeof globalThis.PointerEvent === "undefined") {
class PointerEventPolyfill extends MouseEvent {
constructor(type: string, params: MouseEventInit = {}) {
super(type, params)
}
}
Object.assign(globalThis, { PointerEvent: PointerEventPolyfill })
}

const { CoexistPopup } = await import("@/features/shared/coexist-popup")

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("CoexistPopup", () => {
let container: HTMLDivElement
let root: Root
const onDone = vi.fn()

beforeEach(() => {
vi.clearAllMocks()
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
container = document.createElement("div")
document.body.append(container)
root = createRoot(container)
mockKyPost.mockReturnValue({
json: vi.fn().mockResolvedValue({ success: true }),
})

act(() => {
root.render(
<CoexistPopup
channel="whatsapp"
integrationId="int-1"
onDone={onDone}
workspaceId="ws-1"
/>,
)
})
})

afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
})

const switchEl = () =>
container.querySelector<HTMLButtonElement>('[role="switch"]')
const enableButton = () =>
Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("coexist.enable"),
)
const declineButton = () =>
Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("coexist.decline"),
)

test("renders the AI-reads-synced-history switch defaulted OFF (AI ignores synced history by default)", () => {
expect(switchEl()).not.toBeNull()
expect(switchEl()?.getAttribute("aria-checked")).toBe("false")
})

test("POSTs aiReadsSyncedHistory: false by default when confirming enable", async () => {
await act(async () => {
enableButton()?.click()
await Promise.resolve()
})

expect(mockKyPost).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
json: expect.objectContaining({
workspaceId: "ws-1",
integrationId: "int-1",
enabled: true,
aiReadsSyncedHistory: false,
}),
}),
)
})

test("POSTs aiReadsSyncedHistory: true after toggling the switch on", async () => {
act(() => {
switchEl()?.click()
})
expect(switchEl()?.getAttribute("aria-checked")).toBe("true")

await act(async () => {
enableButton()?.click()
await Promise.resolve()
})

expect(mockKyPost).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
json: expect.objectContaining({ aiReadsSyncedHistory: true }),
}),
)
})

test("POSTs aiReadsSyncedHistory in the decline body too (enabled: false)", async () => {
act(() => {
switchEl()?.click()
})

await act(async () => {
declineButton()?.click()
await Promise.resolve()
})

expect(mockKyPost).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
json: expect.objectContaining({
enabled: false,
aiReadsSyncedHistory: true,
}),
}),
)
})
})
78 changes: 77 additions & 1 deletion apps/builder/__tests__/coexist-whatsapp-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ describe("setCoexistWhatsappAPI", () => {

// Flag flipped on via the atomic UPDATE … RETURNING (one update on success).
expect(db.update).toHaveBeenCalledTimes(1)
expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ coexistEnabled: true })
// aiReadsSyncedHistory defaults to false (zod default) and is written on
// enable — the default advances the AI marker (AI ignores synced history).
expect(dbUpdateBuilder.set).toHaveBeenCalledWith({
coexistEnabled: true,
coexistAiReadsSyncedHistory: false,
})

// CoexistSyncRun insert with the init-row values.
expect(db.insert).toHaveBeenCalledTimes(1)
Expand All @@ -188,6 +193,25 @@ describe("setCoexistWhatsappAPI", () => {
expect(mockQueueAdd).not.toHaveBeenCalled()
})

test("enabled:true, aiReadsSyncedHistory:true — writes coexistAiReadsSyncedHistory alongside coexistEnabled", async () => {
const result = await call(
procedure,
{
workspaceId: "ws-1",
integrationId: "int-1",
enabled: true,
aiReadsSyncedHistory: true,
},
{ context: stubContext },
)

expect(result).toEqual({ success: true })
expect(dbUpdateBuilder.set).toHaveBeenCalledWith({
coexistEnabled: true,
coexistAiReadsSyncedHistory: true,
})
})

test("enabled:false — flag-only: flips coexistEnabled, no run insert, no delete, no job", async () => {
const result = await call(
procedure,
Expand All @@ -208,6 +232,24 @@ describe("setCoexistWhatsappAPI", () => {
expect(mockQueueAdd).not.toHaveBeenCalled()
})

test("enabled:false (decline) — never writes coexistAiReadsSyncedHistory, even when the popup sent aiReadsSyncedHistory:true", async () => {
const result = await call(
procedure,
{
workspaceId: "ws-1",
integrationId: "int-1",
enabled: false,
aiReadsSyncedHistory: true,
},
{ context: stubContext },
)

expect(result).toEqual({ success: true })
// The UPDATE runs BEFORE the `if (enabled)` branch — decline must never
// write coexistAiReadsSyncedHistory, regardless of what the popup sent.
expect(dbUpdateBuilder.set).toHaveBeenCalledWith({ coexistEnabled: false })
})

test("enabled:true — surfaces failure reason and marks the run failed when smb_app_data fails", async () => {
// smb_app_state_sync fails → handler returns the reason and marks the run.
mockTriggerSmbAppDataSync
Expand Down Expand Up @@ -294,6 +336,40 @@ describe("i18n key presence (H12)", () => {
).toBeDefined()
})

test("coexist.aiReadsSyncedHistoryLabel is defined in en.json and vi.json", () => {
expect(
(
enMessages as unknown as Record<string, unknown> & {
coexist: Record<string, unknown>
}
).coexist.aiReadsSyncedHistoryLabel,
).toBeDefined()
expect(
(
viMessages as unknown as Record<string, unknown> & {
coexist: Record<string, unknown>
}
).coexist.aiReadsSyncedHistoryLabel,
).toBeDefined()
})

test("coexist.aiReadsSyncedHistoryHelper is defined in en.json and vi.json", () => {
expect(
(
enMessages as unknown as Record<string, unknown> & {
coexist: Record<string, unknown>
}
).coexist.aiReadsSyncedHistoryHelper,
).toBeDefined()
expect(
(
viMessages as unknown as Record<string, unknown> & {
coexist: Record<string, unknown>
}
).coexist.aiReadsSyncedHistoryHelper,
).toBeDefined()
})

test("whatsapp.fillRequiredFields is defined in en.json and vi.json", () => {
expect(
(
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,8 @@
"toggleHelper": "لا تنجح إعادة التفعيل إلا إذا كانت Meta لا تزال تحتفظ بالبيانات مؤقتًا (خلال 24 ساعة من الإعداد). وإلا، فاقطع الاتصال ثم أعده.",
"toggleHelperMessenger": "لا تنجح إعادة التفعيل إلا إذا كانت Meta لا تزال تحتفظ بالبيانات مؤقتًا (خلال 24 ساعة من الإعداد). وإلا، فاقطع اتصال صفحة Messenger ثم أعده.",
"toggleHelperWhatsapp": "لا تنجح إعادة التفعيل إلا إذا كانت Meta لا تزال تحتفظ بالبيانات مؤقتًا (خلال 24 ساعة من الإعداد). وإلا، فاقطع اتصال رقم WhatsApp ثم أعده.",
"aiReadsSyncedHistoryLabel": "يقرأ الذكاء الاصطناعي السجل المتزامن",
"aiReadsSyncedHistoryHelper": "عند الإيقاف، لا يرى الذكاء الاصطناعي سوى الرسائل الجديدة.",
"success": {
"enabled": "بدأت المزامنة. ستظهر جهات الاتصال والرسائل السابقة قريبًا.",
"disabled": "تم تعطيل المزامنة."
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,8 @@
"toggleHelper": "Re-enabling kun works hvis Meta har buffered data (within 24 timer af onboarding). Otherwise afbryd og forbind igen.",
"toggleHelperMessenger": "Re-enabling kun works hvis Meta har buffered data (within 24 timer af onboarding). Otherwise afbryd og forbind igen den Messenger page.",
"toggleHelperWhatsapp": "Re-enabling kun works hvis Meta har buffered data (within 24 timer af onboarding). Otherwise afbryd og forbind igen den WhatsApp nummer.",
"aiReadsSyncedHistoryLabel": "AI læser synket historik",
"aiReadsSyncedHistoryHelper": "Når slået fra, ser AI kun nye beskeder.",
"success": {
"enabled": "Synkroniser started. Historical kontakter og beskeder vil appear shortly.",
"disabled": "Synkroniser deaktiveret."
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,8 @@
"toggleHelper": "Eine erneute Aktivierung funktioniert nur, wenn Meta Daten gepuffert hat (innerhalb von 24 Stunden nach dem Onboarding). Andernfalls trennen und erneut verbinden.",
"toggleHelperMessenger": "Eine erneute Aktivierung funktioniert nur, wenn Meta Daten gepuffert hat (innerhalb von 24 Stunden nach dem Onboarding). Andernfalls die Messenger-Seite trennen und erneut verbinden.",
"toggleHelperWhatsapp": "Eine erneute Aktivierung funktioniert nur, wenn Meta Daten gepuffert hat (innerhalb von 24 Stunden nach dem Onboarding). Andernfalls die WhatsApp-Nummer trennen und erneut verbinden.",
"aiReadsSyncedHistoryLabel": "KI liest Synchronverlauf",
"aiReadsSyncedHistoryHelper": "Wenn aus, sieht die KI nur neue Nachrichten.",
"success": {
"enabled": "Synchronisierung gestartet. Historische Kontakte und Nachrichten werden in Kürze angezeigt.",
"disabled": "Synchronisierung deaktiviert."
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3041,6 +3041,8 @@
"toggleHelper": "Re-enabling only works if Meta has buffered data (within 24 hours of onboarding). Otherwise disconnect and reconnect.",
"toggleHelperMessenger": "Re-enabling only works if Meta has buffered data (within 24 hours of onboarding). Otherwise disconnect and reconnect the Messenger page.",
"toggleHelperWhatsapp": "Re-enabling only works if Meta has buffered data (within 24 hours of onboarding). Otherwise disconnect and reconnect the WhatsApp number.",
"aiReadsSyncedHistoryLabel": "AI reads synced history",
"aiReadsSyncedHistoryHelper": "When off, AI only sees new messages.",
"success": {
"enabled": "Sync started. Historical contacts and messages will appear shortly.",
"disabled": "Sync disabled."
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,8 @@
"toggleHelper": "Re-enabling only works si Meta tiene buffered datos (within 24 horas de onboarding). Otherwise Desconectar y reconnect.",
"toggleHelperMessenger": "Re-enabling only works si Meta tiene buffered datos (within 24 horas de onboarding). Otherwise Desconectar y reConectar el Messenger página.",
"toggleHelperWhatsapp": "Re-enabling only works si Meta tiene buffered datos (within 24 horas de onboarding). Otherwise Desconectar y reConectar el WhatsApp number.",
"aiReadsSyncedHistoryLabel": "La IA lee el historial sincronizado",
"aiReadsSyncedHistoryHelper": "Si está desactivado, la IA solo ve mensajes nuevos.",
"success": {
"enabled": "Sync started. Historical contactoos y mensajes se appear shortly.",
"disabled": "Sync desactivado."
Expand Down
Loading
Loading