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.
|
// listens to only one trigger runs only when that trigger matches.
|
||||||
if (contactOutcome.wasCreated) automationTriggers.unshift('new_contact_created')
|
if (contactOutcome.wasCreated) automationTriggers.unshift('new_contact_created')
|
||||||
if (isFirstInboundMessage) automationTriggers.unshift('first_inbound_message')
|
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) {
|
for (const triggerType of automationTriggers) {
|
||||||
runAutomationsForTrigger({
|
await runAutomationsForTrigger({
|
||||||
accountId,
|
accountId,
|
||||||
triggerType,
|
triggerType,
|
||||||
contactId: contactRecord.id,
|
contactId: contactRecord.id,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const h = vi.hoisted(() => ({
|
|||||||
fromCalls: [] as string[],
|
fromCalls: [] as string[],
|
||||||
updateCalls: [] as { table: string; filters: [string, string, unknown][] }[],
|
updateCalls: [] as { table: string; filters: [string, string, unknown][] }[],
|
||||||
upsertCalls: [] as { table: string; payload: unknown }[],
|
upsertCalls: [] as { table: string; payload: unknown }[],
|
||||||
|
logInserts: [] as Record<string, unknown>[],
|
||||||
logUpdates: [] 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 === "automations") return { data: state.automations, error: null };
|
||||||
if (table === "automation_logs") {
|
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") {
|
if (type === "update") {
|
||||||
state.logUpdates.push(ops.payload as Record<string, unknown>);
|
state.logUpdates.push(ops.payload as Record<string, unknown>);
|
||||||
return { data: null, error: null };
|
return { data: null, error: null };
|
||||||
@ -113,6 +117,7 @@ beforeEach(() => {
|
|||||||
h.state.fromCalls = [];
|
h.state.fromCalls = [];
|
||||||
h.state.updateCalls = [];
|
h.state.updateCalls = [];
|
||||||
h.state.upsertCalls = [];
|
h.state.upsertCalls = [];
|
||||||
|
h.state.logInserts = [];
|
||||||
h.state.logUpdates = [];
|
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", () => {
|
describe("update_contact_field — custom fields", () => {
|
||||||
it("upserts contact_custom_values when the field is account-owned", async () => {
|
it("upserts contact_custom_values when the field is account-owned", async () => {
|
||||||
h.state.owned = { id: "c1" };
|
h.state.owned = { id: "c1" };
|
||||||
|
|||||||
@ -189,7 +189,15 @@ async function executeAutomation(automation: Automation, input: DispatchInput) {
|
|||||||
contact_id: input.contactId ?? null,
|
contact_id: input.contactId ?? null,
|
||||||
trigger_event: input.triggerType,
|
trigger_event: input.triggerType,
|
||||||
steps_executed: [],
|
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()
|
.select()
|
||||||
.single()
|
.single()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user