Tighten recovery resolution contract

This commit is contained in:
Dotta committed 2026-05-09 23:31:14 -05:00
1 parent 5859f22f5a
commit 4614547f3f
4 files changed
+20 -8

No files matched your search

@@ -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", () => {
-2
View File
@@ -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"
) {
@@ -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,
});
});
@@ -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<Parameters<Db["transaction"]>[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<IssueRecoveryAction | null> {
const now = new Date();
const predicates = [