mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
test: cover liveness recovery owner skips
This commit is contained in:
1 parent
607c3b1608
commit
eccba68be4
2 files changed
+100
-1
No files matched your search
@@ -4,7 +4,9 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest
|
||||
import {
|
||||
activityLog,
|
||||
agents,
|
||||
budgetPolicies,
|
||||
companies,
|
||||
costEvents,
|
||||
createDb,
|
||||
executionWorkspaces,
|
||||
heartbeatRuns,
|
||||
@@ -191,7 +193,7 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => {
|
||||
type: "blocks",
|
||||
});
|
||||
|
||||
return { companyId, managerId, blockedIssueId, blockerIssueId };
|
||||
return { companyId, managerId, coderId, blockedIssueId, blockerIssueId };
|
||||
}
|
||||
|
||||
it("keeps liveness findings advisory when auto recovery is disabled", async () => {
|
||||
@@ -342,6 +344,71 @@ describeEmbeddedPostgres("heartbeat issue graph liveness escalation", () => {
|
||||
expect(events.some((event) => event.action === "issue.blockers.updated")).toBe(true);
|
||||
});
|
||||
|
||||
it("skips budget-blocked direct owners and assigns recovery to the manager fallback", async () => {
|
||||
await enableAutoRecovery();
|
||||
const { companyId, managerId, coderId, blockedIssueId, blockerIssueId } = await seedBlockedChain();
|
||||
const issueTimestamp = new Date(Date.now() - 25 * 60 * 60 * 1000);
|
||||
await db
|
||||
.update(issues)
|
||||
.set({
|
||||
status: "in_review",
|
||||
assigneeAgentId: coderId,
|
||||
updatedAt: issueTimestamp,
|
||||
})
|
||||
.where(eq(issues.id, blockerIssueId));
|
||||
await db.insert(budgetPolicies).values({
|
||||
companyId,
|
||||
scopeType: "agent",
|
||||
scopeId: coderId,
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: 1,
|
||||
hardStopEnabled: true,
|
||||
isActive: true,
|
||||
});
|
||||
await db.insert(costEvents).values({
|
||||
companyId,
|
||||
agentId: coderId,
|
||||
issueId: blockerIssueId,
|
||||
provider: "test",
|
||||
biller: "test",
|
||||
billingType: "tokens",
|
||||
model: "test-model",
|
||||
costCents: 1,
|
||||
occurredAt: new Date(),
|
||||
});
|
||||
|
||||
const result = await heartbeatService(db).reconcileIssueGraphLiveness();
|
||||
|
||||
expect(result.escalationsCreated).toBe(1);
|
||||
const escalations = await db
|
||||
.select()
|
||||
.from(issues)
|
||||
.where(and(eq(issues.companyId, companyId), eq(issues.originKind, "harness_liveness_escalation")));
|
||||
expect(escalations).toHaveLength(1);
|
||||
expect(escalations[0]).toMatchObject({
|
||||
parentId: blockerIssueId,
|
||||
assigneeAgentId: managerId,
|
||||
originId: [
|
||||
"harness_liveness",
|
||||
companyId,
|
||||
blockedIssueId,
|
||||
"in_review_without_action_path",
|
||||
blockerIssueId,
|
||||
].join(":"),
|
||||
});
|
||||
|
||||
const events = await db.select().from(activityLog).where(eq(activityLog.companyId, companyId));
|
||||
const createdEvent = events.find((event) => event.action === "issue.harness_liveness_escalation_created");
|
||||
expect(createdEvent?.details).toMatchObject({
|
||||
ownerSelection: {
|
||||
selectedAgentId: managerId,
|
||||
selectedReason: "assignee_reporting_chain",
|
||||
budgetBlockedCandidateAgentIds: [coderId],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("parents recovery under the leaf blocker without inheriting dependent or blocker execution state for manager-owned recovery", async () => {
|
||||
await enableAutoRecovery();
|
||||
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
|
||||
|
||||
@@ -286,6 +286,38 @@ describe("issue graph liveness classifier", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips paused stalled review assignees when choosing recovery owner candidates", () => {
|
||||
const reviewIssueId = "review-1";
|
||||
|
||||
const findings = classifyIssueGraphLiveness({
|
||||
issues: [
|
||||
issue({
|
||||
id: reviewIssueId,
|
||||
identifier: "PAP-2279",
|
||||
title: "Screenshot acceptance review",
|
||||
status: "in_review",
|
||||
assigneeAgentId: coderId,
|
||||
executionState: null,
|
||||
}),
|
||||
],
|
||||
relations: [],
|
||||
agents: [agent({ status: "paused" }), manager],
|
||||
});
|
||||
|
||||
expect(findings).toHaveLength(1);
|
||||
expect(findings[0]).toMatchObject({
|
||||
state: "in_review_without_action_path",
|
||||
recommendedOwnerAgentId: managerId,
|
||||
});
|
||||
expect(findings[0]?.recommendedOwnerCandidates).toEqual([
|
||||
{
|
||||
agentId: managerId,
|
||||
reason: "assignee_reporting_chain",
|
||||
sourceIssueId: reviewIssueId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not flag healthy in_review issues with an explicit action path", () => {
|
||||
const reviewIssueId = "review-1";
|
||||
const baseReviewIssue = issue({
|
||||
|
||||
Reference in new issue
Block a user