Skip to content
Open
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
68 changes: 66 additions & 2 deletions apps/worker/__tests__/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ describe("seekConnectedNode", () => {
})

describe("MESSAGE_PRODUCING_STEP_TYPES", () => {
test("matches exactly the step types mapped to sendFlowMessage in flowStepHandlers", async () => {
test("matches sendFlowMessage steps plus getUserData prompt-production exception", async () => {
const { MESSAGE_PRODUCING_STEP_TYPES } = await import(
"../src/integration/handlers/flow-utils"
)
Expand All @@ -602,7 +602,71 @@ describe("MESSAGE_PRODUCING_STEP_TYPES", () => {
)

expect(new Set(MESSAGE_PRODUCING_STEP_TYPES)).toEqual(
actualMessageProducingTypes,
new Set([...actualMessageProducingTypes, "getUserData"]),
)
})
})

describe("executeMultipleSteps — comment anchor lifecycle", () => {
test("getUserData receives unspent anchor and marks it spent for subsequent steps", async () => {
const { flowStepHandlers } = await import(
"../src/integration/handlers/step"
)
const getUserDataSpy = mockSpy(
flowStepHandlers,
"getUserData",
).mockResolvedValue({
status: "success",
result: null,
})
const sendTextSpy = mockSpy(flowStepHandlers, "sendText").mockResolvedValue(
undefined,
)

const anchor = {
commentId: "comment-1",
replyChannel: "private" as const,
}
const result = await executeMultipleSteps({
...makeBaseProps(),
commentAnchor: anchor,
steps: [makeStep("getUserData"), makeStep("sendText")],
})

expect(getUserDataSpy).toHaveBeenCalledWith(
expect.objectContaining({ commentAnchor: anchor }),
)
expect(sendTextSpy).toHaveBeenCalledWith(
expect.objectContaining({ commentAnchor: { ...anchor, spent: true } }),
)
expect(result?.commentAnchor).toEqual({ ...anchor, spent: true })
})

test("getUserData after sendText receives spent anchor", async () => {
const { flowStepHandlers } = await import(
"../src/integration/handlers/step"
)
mockSpy(flowStepHandlers, "sendText").mockResolvedValue(undefined)
const getUserDataSpy = mockSpy(
flowStepHandlers,
"getUserData",
).mockResolvedValue({
status: "wait",
result: null,
})
const anchor = {
commentId: "comment-1",
replyChannel: "private" as const,
}

await executeMultipleSteps({
...makeBaseProps(),
commentAnchor: anchor,
steps: [makeStep("sendText"), makeStep("getUserData")],
})

expect(getUserDataSpy).toHaveBeenCalledWith(
expect.objectContaining({ commentAnchor: { ...anchor, spent: true } }),
)
})
})
Expand Down
34 changes: 34 additions & 0 deletions apps/worker/__tests__/get-user-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,40 @@ describe("getUserData — validation logic", () => {
lastMessage.current = null
})

test.each([
["unspent", { commentId: "comment-1", replyChannel: "private" as const }],
[
"spent",
{ commentId: "comment-1", replyChannel: "private" as const, spent: true },
],
])("forwards %s comment anchor through flow message queue", async (_label, commentAnchor) => {
const props = {
...makeProps(ReplyFormat.email, {}, 0),
ctx: undefined,
commentAnchor,
}

await getUserData(props)

expect(chatQueueAdd).toHaveBeenCalledWith("sendFlowMessage", {
type: "sendFlowMessage",
data: expect.objectContaining({ commentAnchor }),
})
})

test("does not include comment anchor for normal flow prompt", async () => {
await getUserData({
...makeProps(ReplyFormat.email, {}, 0),
ctx: undefined,
})

const [, job] = chatQueueAdd.mock.calls[0] as [
string,
{ data: Record<string, unknown> },
]
expect(job.data).not.toHaveProperty("commentAnchor")
})

test("anchors the message lookup on conversation.lastActivityAt, not contactInbox", async () => {
lastMessage.current = makeIncomingMessage({ text: "user@example.com" })
const props = makeProps(ReplyFormat.email)
Expand Down
4 changes: 3 additions & 1 deletion apps/worker/src/integration/handlers/flow-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ export type HeavyStepProps<T> = ExecuteStepProps<T> & {
/**
* Step types that actually send an outgoing message via `sendFlowMessage`
* (see the `flowStepHandlers` map in `./step.ts` — keep this set in sync with
* every entry mapped to `sendFlowMessage` there). Used to decide which step
* every entry mapped to `sendFlowMessage` there, plus prompt-producing
* `getUserData`). Used to decide which step
* "claims" a pending `commentAnchor` (comment-triggered private-reply flow) as
* its first outgoing message.
*/
export const MESSAGE_PRODUCING_STEP_TYPES = new Set<StepType>([
stepTypes.enum.getUserData,
stepTypes.enum.sendText,
stepTypes.enum.sendImage,
stepTypes.enum.sendMultipleImages,
Expand Down
1 change: 1 addition & 0 deletions apps/worker/src/integration/handlers/get-user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ async function sendMessage(
flowVersionId,
step: promptStep,
metadata: props.metadata,
...(props.commentAnchor ? { commentAnchor: props.commentAnchor } : {}),
...(props.appointmentId ? { appointmentId: props.appointmentId } : {}),
})
}
Expand Down