Skip to content

Commit 8f28f45

Browse files
committed
refactor(worker): move integration, chat and ai-agent handler data access into business
Removes direct db usage from apps/worker/src/{integration,chat,ai-agent, services,lib} per .agents/rules/data-access.md. Hot-path bodies are moved verbatim; every isNull TOCTOU guard, sql increment, untargeted onConflictDoNothing, sql.identifier and withBlockedOwnerGuard call site is preserved. - coexist: claimRunForSync/incrementProgress/findInitState/... on the coexist-sync-run repository, staging repository methods, and a new coexistImportService holding the historical-import transaction - contact/tag/sequence flow steps, inbox labels, templates, message status and received-message activity tracking routed through contactService, tagService, contactSequenceService, conversationService, inboxService and the integration services - sequenceDispatchUtils gains findRunning/markCompleted/markCanceled/ markFailed; new ai-embedding, integration-lookup and import repositories; lib/db.ts keeps its exported names and signatures Deliberate behavior changes (recorded in the PR): stepBlockContact now goes through contactService.block (emits + invalidates), setFlowFlags invalidates the contact cache, process-ai-file enqueues only the ids it inserted, send-flow-direct and update-avatar add workspace scoping, and the inbound/outbound activity conversation updates add workspaceId to their WHERE.
1 parent 2f72455 commit 8f28f45

87 files changed

Lines changed: 4507 additions & 3273 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/worker/__tests__/broadcast-step.test.ts

Lines changed: 28 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,20 @@
11
import { beforeEach, describe, expect, test, vi } from "vitest"
22
import { z } from "zod"
33

4-
const setSpy = vi.fn<(values: Record<string, unknown>) => unknown>()
5-
const whereSpy = vi.fn<(...args: unknown[]) => unknown>()
6-
const updateSpy = vi.fn<(table: unknown) => unknown>()
4+
const subscribeBroadcastIfUnsubscribed = vi.fn()
5+
const unsubscribeBroadcastSpy = vi.fn()
76

7+
// Never call the real db client — this test never reaches it (contact.ts no
8+
// longer imports it directly), but a transitive import chain still resolves
9+
// the module, which would otherwise open a real DB connection at import time.
810
vi.mock("@chatbotx.io/database/client", () => ({
9-
db: {
10-
update: (table: unknown) => {
11-
updateSpy(table)
12-
return {
13-
set: (values: Record<string, unknown>) => {
14-
setSpy(values)
15-
return {
16-
where: (...args: unknown[]) => {
17-
whereSpy(...args)
18-
return Promise.resolve()
19-
},
20-
}
21-
},
22-
}
23-
},
24-
},
11+
db: {},
2512
and: (...args: unknown[]) => ({ __and: args }),
2613
eq: (column: unknown, value: unknown) => ({ __eq: [column, value] }),
2714
isNull: (column: unknown) => ({ __isNull: column }),
15+
inArray: (column: unknown, values: unknown) => ({
16+
__inArray: [column, values],
17+
}),
2818
}))
2919

3020
// Do NOT importOriginal the real schema module here: its index pulls in the
@@ -193,7 +183,11 @@ vi.mock("@chatbotx.io/business", () => ({
193183
// The rest below are never called by this test — only needed so the real
194184
// (unmocked) contactVariableService import chain (@chatbotx.io/variables)
195185
// resolves without throwing on missing exports.
196-
contactService: { delete: vi.fn() },
186+
contactService: {
187+
delete: vi.fn(),
188+
subscribeBroadcastIfUnsubscribed,
189+
unsubscribeBroadcast: unsubscribeBroadcastSpy,
190+
},
197191
contactCustomFieldService: {
198192
setValueByKey: vi.fn(),
199193
deleteByKey: vi.fn(),
@@ -253,56 +247,32 @@ const buildProps = () =>
253247
>[0]
254248

255249
beforeEach(() => {
256-
updateSpy.mockClear()
257-
setSpy.mockClear()
258-
whereSpy.mockClear()
250+
subscribeBroadcastIfUnsubscribed.mockClear()
251+
unsubscribeBroadcastSpy.mockClear()
259252
emitContactUnsubscribed.mockClear()
260253
})
261254

262255
describe("subscribeBroadcast", () => {
263-
test("sets broadcastSubscribedAt to current Date scoped by contact + workspace", async () => {
264-
const before = Date.now()
256+
test("delegates to contactService.subscribeBroadcastIfUnsubscribed scoped by contact + workspace", async () => {
265257
await subscribeBroadcast(buildProps())
266-
const after = Date.now()
267-
268-
expect(updateSpy).toHaveBeenCalledTimes(1)
269-
expect(setSpy).toHaveBeenCalledTimes(1)
270-
expect(whereSpy).toHaveBeenCalledTimes(1)
271258

272-
const setCall = setSpy.mock.calls[0][0]
273-
expect(setCall.broadcastSubscribedAt).toBeInstanceOf(Date)
274-
const ts = (setCall.broadcastSubscribedAt as Date).getTime()
275-
expect(ts).toBeGreaterThanOrEqual(before)
276-
expect(ts).toBeLessThanOrEqual(after)
277-
278-
const whereArg = whereSpy.mock.calls[0][0] as { __and: unknown[] }
279-
expect(whereArg.__and).toHaveLength(3)
280-
})
281-
282-
test("is idempotent — WHERE includes isNull guard to preserve original subscription date", async () => {
283-
await subscribeBroadcast(buildProps())
284-
285-
const whereArg = whereSpy.mock.calls[0][0] as {
286-
__and: Array<{ __isNull?: unknown }>
287-
}
288-
const hasIsNullGuard = whereArg.__and.some((c) => "__isNull" in c)
289-
expect(hasIsNullGuard).toBe(true)
259+
expect(subscribeBroadcastIfUnsubscribed).toHaveBeenCalledTimes(1)
260+
expect(subscribeBroadcastIfUnsubscribed).toHaveBeenCalledWith({
261+
workspaceId: "workspace-1",
262+
contactId: "contact-1",
263+
})
290264
})
291265
})
292266

293267
describe("unsubscribeBroadcast", () => {
294-
test("sets broadcastSubscribedAt to null scoped by contact + workspace", async () => {
268+
test("delegates to contactService.unsubscribeBroadcast scoped by contact + workspace", async () => {
295269
await unsubscribeBroadcast(buildProps())
296270

297-
expect(updateSpy).toHaveBeenCalledTimes(1)
298-
expect(setSpy).toHaveBeenCalledTimes(1)
299-
expect(whereSpy).toHaveBeenCalledTimes(1)
300-
301-
const setCall = setSpy.mock.calls[0][0]
302-
expect(setCall.broadcastSubscribedAt).toBeNull()
303-
304-
const whereArg = whereSpy.mock.calls[0][0] as { __and: unknown[] }
305-
expect(whereArg.__and).toHaveLength(2)
271+
expect(unsubscribeBroadcastSpy).toHaveBeenCalledTimes(1)
272+
expect(unsubscribeBroadcastSpy).toHaveBeenCalledWith({
273+
workspaceId: "workspace-1",
274+
contactId: "contact-1",
275+
})
306276
expect(emitContactUnsubscribed).toHaveBeenCalledWith(
307277
"workspace-1",
308278
"contact-1",

0 commit comments

Comments
 (0)