mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Clear stale pipeline automation health failures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user