diff --git a/packages/shared/src/validators/issue.test.ts b/packages/shared/src/validators/issue.test.ts index d4f5921f8c9..fe962339dac 100644 --- a/packages/shared/src/validators/issue.test.ts +++ b/packages/shared/src/validators/issue.test.ts @@ -64,6 +64,12 @@ describe("issue validators", () => { sourceIssueStatus: "blocked", }).success, ).toBe(false); + + expect( + resolveIssueRecoveryActionSchema.safeParse({ + outcome: "false_positive", + }).success, + ).toBe(false); }); it("allows cancelled recovery resolutions to atomically restore the source issue status", () => { @@ -83,6 +89,12 @@ describe("issue validators", () => { sourceIssueStatus: "blocked", }).success, ).toBe(false); + + expect( + resolveIssueRecoveryActionSchema.safeParse({ + outcome: "cancelled", + }).success, + ).toBe(false); }); it("rejects recovery outcomes that are not supported by the source-scoped resolution endpoint", () => { diff --git a/packages/shared/src/validators/issue.ts b/packages/shared/src/validators/issue.ts index 4e535334fe7..4364863cfb7 100644 --- a/packages/shared/src/validators/issue.ts +++ b/packages/shared/src/validators/issue.ts @@ -239,8 +239,6 @@ export const resolveIssueRecoveryActionSchema = z.object({ if (value.outcome === "false_positive" || value.outcome === "cancelled") { if ( - value.sourceIssueStatus !== undefined && - value.sourceIssueStatus !== null && value.sourceIssueStatus !== "done" && value.sourceIssueStatus !== "in_review" ) { diff --git a/server/src/__tests__/issue-recovery-actions.test.ts b/server/src/__tests__/issue-recovery-actions.test.ts index b0621a19490..cc533998ba0 100644 --- a/server/src/__tests__/issue-recovery-actions.test.ts +++ b/server/src/__tests__/issue-recovery-actions.test.ts @@ -645,7 +645,7 @@ describeEmbeddedPostgres("issue recovery actions", () => { expect(await recoveryActionSvc.getActiveForIssue(companyId, sourceIssueId)).toBeNull(); }); - it("records false positives without cancelling the source issue", async () => { + it("rejects false-positive recovery resolution without an explicit source issue status", async () => { const { companyId, managerId, sourceIssueId } = await seedCompany(); const recoveryActionSvc = issueRecoveryActionService(db); const action = await recoveryActionSvc.upsertSourceScoped({ @@ -669,7 +669,7 @@ describeEmbeddedPostgres("issue recovery actions", () => { outcome: "false_positive", resolutionNote: "The source issue still has a live execution path.", }) - .expect(200); + .expect(400); const [sourceIssue] = await db.select().from(issues).where(eq(issues.id, sourceIssueId)); expect(sourceIssue?.status).toBe("in_progress"); @@ -679,9 +679,9 @@ describeEmbeddedPostgres("issue recovery actions", () => { .from(issueRecoveryActions) .where(eq(issueRecoveryActions.id, action.id)); expect(actionRow).toMatchObject({ - status: "resolved", - outcome: "false_positive", - resolutionNote: "The source issue still has a live execution path.", + status: "active", + outcome: null, + resolutionNote: null, }); }); diff --git a/server/src/services/issue-recovery-actions.ts b/server/src/services/issue-recovery-actions.ts index 30f04ec70a2..1e39c066f2b 100644 --- a/server/src/services/issue-recovery-actions.ts +++ b/server/src/services/issue-recovery-actions.ts @@ -13,6 +13,8 @@ const ACTIVE_RECOVERY_ACTION_STATUSES = ["active", "escalated"] as const satisfi const MAX_UPSERT_RETRIES = 3; type IssueRecoveryActionRow = typeof issueRecoveryActions.$inferSelect; +type DbTransaction = Parameters[0]>[0]; +type DbOrTransaction = Db | DbTransaction; export type UpsertIssueRecoveryActionInput = { companyId: string; @@ -225,7 +227,7 @@ export function issueRecoveryActionService(db: Db) { async function resolveActiveForIssue( input: ResolveIssueRecoveryActionInput, - dbOrTx: any = db, + dbOrTx: DbOrTransaction = db, ): Promise { const now = new Date(); const predicates = [