mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge pull request #444 from ArnasDon/fix/409-automation-dispatch-reliability
fix(automations): stop inbound runs being dropped and mislogged
This commit is contained in:
commit
fc1f19b72a
@ -781,8 +781,16 @@ async function processMessage(
|
||||
// listens to only one trigger runs only when that trigger matches.
|
||||
if (contactOutcome.wasCreated) automationTriggers.unshift('new_contact_created')
|
||||
if (isFirstInboundMessage) automationTriggers.unshift('first_inbound_message')
|
||||
// Awaited — not fire-and-forget. We're inside the route's `after()`
|
||||
// block, which only keeps the function alive for promises it can see, so
|
||||
// a detached dispatch can be frozen part-way through: the log row is
|
||||
// inserted, then the steps never run. That is issue #301's failure mode
|
||||
// recurring one level down, and it's what issue #409 reported as runs
|
||||
// logging zero steps. `runAutomationsForTrigger` owns its own try/catch
|
||||
// and never throws; the `.catch` is belt-and-braces so one trigger
|
||||
// type's failure can't skip the rest of the loop.
|
||||
for (const triggerType of automationTriggers) {
|
||||
runAutomationsForTrigger({
|
||||
await runAutomationsForTrigger({
|
||||
accountId,
|
||||
triggerType,
|
||||
contactId: contactRecord.id,
|
||||
|
||||
@ -11,6 +11,7 @@ const h = vi.hoisted(() => ({
|
||||
fromCalls: [] as string[],
|
||||
updateCalls: [] as { table: string; filters: [string, string, unknown][] }[],
|
||||
upsertCalls: [] as { table: string; payload: unknown }[],
|
||||
logInserts: [] as Record<string, unknown>[],
|
||||
logUpdates: [] as Record<string, unknown>[],
|
||||
},
|
||||
}));
|
||||
@ -46,7 +47,10 @@ vi.mock("./admin-client", () => {
|
||||
}
|
||||
if (table === "automations") return { data: state.automations, error: null };
|
||||
if (table === "automation_logs") {
|
||||
if (type === "insert") return { data: { id: "log1" }, error: null };
|
||||
if (type === "insert") {
|
||||
state.logInserts.push(ops.payload as Record<string, unknown>);
|
||||
return { data: { id: "log1" }, error: null };
|
||||
}
|
||||
if (type === "update") {
|
||||
state.logUpdates.push(ops.payload as Record<string, unknown>);
|
||||
return { data: null, error: null };
|
||||
@ -113,6 +117,7 @@ beforeEach(() => {
|
||||
h.state.fromCalls = [];
|
||||
h.state.updateCalls = [];
|
||||
h.state.upsertCalls = [];
|
||||
h.state.logInserts = [];
|
||||
h.state.logUpdates = [];
|
||||
});
|
||||
|
||||
@ -170,6 +175,47 @@ describe("runAutomationsForTrigger — tenant isolation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("automation_logs — status is seeded pessimistically (issue #409)", () => {
|
||||
it("writes the log row as 'failed' before any step runs", async () => {
|
||||
h.state.owned = { id: "c1" };
|
||||
h.state.automations = [automationWithUpdateStep()];
|
||||
h.state.steps = [updateStep()];
|
||||
|
||||
await runAutomationsForTrigger({
|
||||
accountId: ACCOUNT,
|
||||
triggerType: "new_message_received",
|
||||
contactId: "c1",
|
||||
context: {},
|
||||
});
|
||||
|
||||
// The insert happens before execution, so a run killed mid-flight must
|
||||
// not leave behind a row that claims it succeeded.
|
||||
expect(h.state.logInserts).toHaveLength(1);
|
||||
expect(h.state.logInserts[0]).toMatchObject({
|
||||
status: "failed",
|
||||
steps_executed: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("still promotes the log to 'success' once the steps complete", async () => {
|
||||
h.state.owned = { id: "c1" };
|
||||
h.state.automations = [automationWithUpdateStep()];
|
||||
h.state.steps = [updateStep()];
|
||||
|
||||
await runAutomationsForTrigger({
|
||||
accountId: ACCOUNT,
|
||||
triggerType: "new_message_received",
|
||||
contactId: "c1",
|
||||
context: {},
|
||||
});
|
||||
|
||||
// The seed is only a floor — the outermost scope still writes the real
|
||||
// verdict, so a completed run reports success as it always did.
|
||||
const withStatus = h.state.logUpdates.filter((u) => "status" in u);
|
||||
expect(withStatus.at(-1)).toMatchObject({ status: "success" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("update_contact_field — custom fields", () => {
|
||||
it("upserts contact_custom_values when the field is account-owned", async () => {
|
||||
h.state.owned = { id: "c1" };
|
||||
|
||||
@ -189,7 +189,15 @@ async function executeAutomation(automation: Automation, input: DispatchInput) {
|
||||
contact_id: input.contactId ?? null,
|
||||
trigger_event: input.triggerType,
|
||||
steps_executed: [],
|
||||
status: 'success',
|
||||
// Seeded pessimistically. The row is written BEFORE any step runs,
|
||||
// and every terminal path below overwrites it (`appendResults` at
|
||||
// the outermost scope, or `finalizeLog`). Seeding 'success' meant a
|
||||
// run that died mid-flight — the process frozen, the pod recycled —
|
||||
// left a permanent `status: 'success'` with `steps_executed: []`,
|
||||
// indistinguishable from an automation that genuinely had nothing
|
||||
// to do. 'failed' inverts that: the status only becomes success if
|
||||
// execution actually reached the end. See issue #409.
|
||||
status: 'failed',
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user