Clear stale pipeline automation health failures

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-01 16:56:28 +00:00
1 parent adbbf12403
commit 19cc090f70
2 files changed
+115 -2

No files matched your search

@@ -175,11 +175,14 @@ describeEmbeddedPostgres("pipeline routes", () => {
const company = await seedCompany();
const agent = await seedAutomationAgent(company.id);
const http = request(app(boardActor));
const stageConfiguredAt = new Date("2026-06-30T17:00:00.000Z");
const [routine] = await db.insert(routines).values({
companyId: company.id,
title: "Assembly automation",
description: "Assemble the item.",
assigneeAgentId: agent.id,
createdAt: stageConfiguredAt,
updatedAt: stageConfiguredAt,
}).returning();
const [pipeline] = await db.insert(pipelines).values({
@@ -197,6 +200,8 @@ describeEmbeddedPostgres("pipeline routes", () => {
kind: "working",
position: 100,
config: { onEnter: { type: "run_routine", routineId: routine!.id } },
createdAt: stageConfiguredAt,
updatedAt: stageConfiguredAt,
},
{
pipelineId,
@@ -204,6 +209,8 @@ describeEmbeddedPostgres("pipeline routes", () => {
name: "Content Review",
kind: "review",
position: 200,
createdAt: stageConfiguredAt,
updatedAt: stageConfiguredAt,
},
]).returning();
const automationId = `${assemblyStage.id}:on_enter`;
@@ -283,6 +290,93 @@ describeEmbeddedPostgres("pipeline routes", () => {
]);
});
it("clears stale automation failures after the automation setup is edited", async () => {
const company = await seedCompany();
const agent = await seedAutomationAgent(company.id);
const http = request(app(boardActor));
const stageConfiguredAt = new Date("2026-06-30T17:00:00.000Z");
const [routine] = await db.insert(routines).values({
companyId: company.id,
title: "Draft automation",
description: "Draft the item.",
assigneeAgentId: agent.id,
createdAt: stageConfiguredAt,
updatedAt: stageConfiguredAt,
}).returning();
const [pipeline] = await db.insert(pipelines).values({
companyId: company.id,
key: "content",
name: "Content",
createdByUserId: "board-user",
}).returning();
const pipelineId = pipeline!.id;
const [draftStage, reviewStage] = await db.insert(pipelineStages).values([
{
pipelineId,
key: "draft",
name: "Draft",
kind: "working",
position: 100,
config: { onEnter: { type: "run_routine", routineId: routine!.id } },
createdAt: stageConfiguredAt,
updatedAt: stageConfiguredAt,
},
{
pipelineId,
key: "review",
name: "Review",
kind: "review",
position: 200,
createdAt: stageConfiguredAt,
updatedAt: stageConfiguredAt,
},
]).returning();
const automationId = `${draftStage!.id}:on_enter`;
const [pipelineCase] = await db.insert(pipelineCases).values({
companyId: company.id,
pipelineId,
stageId: reviewStage!.id,
caseKey: "fixed-config",
title: "Config was fixed",
}).returning();
await db.insert(pipelineAutomationExecutions).values({
companyId: company.id,
caseId: pipelineCase!.id,
automationId,
triggeringEventId: randomUUID(),
routineId: routine!.id,
status: "failed",
error: "automation_not_configured",
createdAt: new Date("2026-06-30T18:00:00.000Z"),
updatedAt: new Date("2026-06-30T18:00:00.000Z"),
});
await http.get(`/api/pipelines/${pipelineId}/health`)
.expect(200)
.expect((res) => {
expect(res.body.warnings).toEqual([
expect.objectContaining({
code: "automation_failed",
stageId: draftStage!.id,
href: `/pipelines/${pipelineId}/items/${pipelineCase!.id}`,
}),
]);
});
await db.update(pipelineStages)
.set({
config: { onEnter: { type: "run_routine", routineId: routine!.id } },
updatedAt: new Date("2026-06-30T19:00:00.000Z"),
})
.where(eq(pipelineStages.id, draftStage!.id));
const health = await http.get(`/api/pipelines/${pipelineId}/health`).expect(200);
expect(health.body.warnings.filter((warning: { code: string }) => warning.code === "automation_failed")).toEqual([]);
});
it("exposes the pipeline and case route surface", async () => {
const company = await seedCompany();
const http = request(app(boardActor));
+21 -2
View File
@@ -1056,6 +1056,7 @@ export function pipelineRoutes(db: Db, options: Parameters<typeof pipelineServic
currentStageKey: pipelineStages.key,
currentStageName: pipelineStages.name,
automationId: pipelineAutomationExecutions.automationId,
routineId: pipelineAutomationExecutions.routineId,
status: pipelineAutomationExecutions.status,
error: pipelineAutomationExecutions.error,
updatedAt: pipelineAutomationExecutions.updatedAt,
@@ -1089,6 +1090,7 @@ export function pipelineRoutes(db: Db, options: Parameters<typeof pipelineServic
env: routines.env,
latestRevisionId: routines.latestRevisionId,
latestRevisionNumber: routines.latestRevisionNumber,
updatedAt: routines.updatedAt,
})
.from(routines)
.where(and(eq(routines.companyId, companyId), inArray(routines.id, automationRoutineIds)))
@@ -1102,8 +1104,10 @@ export function pipelineRoutes(db: Db, options: Parameters<typeof pipelineServic
env: row.env,
latestRevisionId: row.latestRevisionId,
latestRevisionNumber: row.latestRevisionNumber,
updatedAt: row.updatedAt,
},
]));
const routineUpdatedAtById = new Map(routineRows.map((row) => [row.id, row.updatedAt]));
const bodyByStageId = new Map<string, string>();
for (const doc of instructionDocs) {
@@ -1143,6 +1147,11 @@ export function pipelineRoutes(db: Db, options: Parameters<typeof pipelineServic
};
});
const stageById = new Map(stages.map((stage) => [stage.id, stage]));
const stageByAutomationId = new Map<string, typeof stages[number]>();
for (const stage of stages) {
const automationId = stageAutomationId(stage);
if (automationId) stageByAutomationId.set(automationId, stage);
}
const latestExecutionByCaseAutomation = new Map<string, typeof automationExecutionRows[number]>();
for (const row of automationExecutionRows) {
const key = `${row.caseId}:${row.automationId}`;
@@ -1152,10 +1161,20 @@ export function pipelineRoutes(db: Db, options: Parameters<typeof pipelineServic
}
}
const failedAutomations: PipelineHealthFailedAutomationInput[] = [...latestExecutionByCaseAutomation.values()]
.filter((row) => row.status === "failed")
.filter((row) => {
if (row.status !== "failed") return false;
const automationStage = stageByAutomationId.get(row.automationId);
if (!automationStage) return false;
const routineUpdatedAt = routineUpdatedAtById.get(row.routineId);
const configurationUpdatedAt = Math.max(
automationStage.updatedAt.getTime(),
routineUpdatedAt?.getTime() ?? 0,
);
return row.updatedAt.getTime() >= configurationUpdatedAt;
})
.map((row) => {
const automationStageId = row.automationId.split(":")[0] ?? "";
const automationStage = stageById.get(automationStageId);
const automationStage = stageByAutomationId.get(row.automationId) ?? stageById.get(automationStageId);
return {
stageId: automationStage?.id ?? row.currentStageId,
stageKey: automationStage?.key ?? row.currentStageKey,