fix: preserve session truth over PR aliases (#118)
This commit is contained in:
parent
a45eb322bb
commit
7ccbe5b260
|
|
@ -766,6 +766,47 @@ describe("check (single session)", () => {
|
|||
expect(notifier.notify).toHaveBeenCalledWith(expect.objectContaining({ type: "pr.closed" }));
|
||||
});
|
||||
|
||||
it("routes closed PR transitions through the pr-closed reaction key", async () => {
|
||||
const notifier = createMockNotifier();
|
||||
const mockSCM = createMockSCM({ getPRState: vi.fn().mockResolvedValue("closed") });
|
||||
const registry = createMockRegistry({
|
||||
runtime: plugins.runtime,
|
||||
agent: plugins.agent,
|
||||
scm: mockSCM,
|
||||
notifier,
|
||||
});
|
||||
|
||||
const session = makeSession({ status: "pr_open", pr: makePR() });
|
||||
const lm = setupCheck("app-1", {
|
||||
session,
|
||||
registry,
|
||||
configOverride: {
|
||||
...config,
|
||||
reactions: {
|
||||
...config.reactions,
|
||||
"pr-closed": {
|
||||
auto: true,
|
||||
action: "notify",
|
||||
priority: "action",
|
||||
},
|
||||
},
|
||||
notificationRouting: {
|
||||
...config.notificationRouting,
|
||||
action: ["desktop"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await lm.check("app-1");
|
||||
|
||||
expect(notifier.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "reaction.triggered",
|
||||
data: expect.objectContaining({ reactionKey: "pr-closed" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects mergeable when approved + CI green", async () => {
|
||||
const mockSCM = createMockSCM({
|
||||
getReviewDecision: vi.fn().mockResolvedValue("approved"),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { createInitialCanonicalLifecycle, deriveLegacyStatus } from "../lifecycle-state.js";
|
||||
|
||||
function createOpenPRLifecycle() {
|
||||
const lifecycle = createInitialCanonicalLifecycle("worker", new Date("2025-01-01T00:00:00Z"));
|
||||
lifecycle.session.startedAt = lifecycle.session.lastTransitionAt;
|
||||
lifecycle.pr.state = "open";
|
||||
lifecycle.pr.reason = "review_pending";
|
||||
lifecycle.pr.number = 42;
|
||||
lifecycle.pr.url = "https://github.com/org/repo/pull/42";
|
||||
lifecycle.pr.lastObservedAt = lifecycle.session.lastTransitionAt;
|
||||
lifecycle.runtime.state = "alive";
|
||||
lifecycle.runtime.reason = "process_running";
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
describe("deriveLegacyStatus", () => {
|
||||
it("preserves urgent session states ahead of open PR aliases", () => {
|
||||
const needsInput = createOpenPRLifecycle();
|
||||
needsInput.session.state = "needs_input";
|
||||
needsInput.session.reason = "awaiting_user_input";
|
||||
|
||||
const stuck = createOpenPRLifecycle();
|
||||
stuck.session.state = "stuck";
|
||||
stuck.session.reason = "probe_failure";
|
||||
|
||||
const terminated = createOpenPRLifecycle();
|
||||
terminated.session.state = "terminated";
|
||||
terminated.session.reason = "manually_killed";
|
||||
|
||||
expect(deriveLegacyStatus(needsInput)).toBe("needs_input");
|
||||
expect(deriveLegacyStatus(stuck)).toBe("stuck");
|
||||
expect(deriveLegacyStatus(terminated)).toBe("terminated");
|
||||
});
|
||||
|
||||
it("keeps PR-oriented aliases for idle workers with open PRs", () => {
|
||||
const reviewPending = createOpenPRLifecycle();
|
||||
reviewPending.session.state = "idle";
|
||||
reviewPending.session.reason = "awaiting_external_review";
|
||||
|
||||
const mergeReady = createOpenPRLifecycle();
|
||||
mergeReady.session.state = "idle";
|
||||
mergeReady.session.reason = "awaiting_external_review";
|
||||
mergeReady.pr.reason = "merge_ready";
|
||||
|
||||
expect(deriveLegacyStatus(reviewPending)).toBe("review_pending");
|
||||
expect(deriveLegacyStatus(mergeReady)).toBe("mergeable");
|
||||
});
|
||||
});
|
||||
|
|
@ -548,6 +548,13 @@ function validateProjectUniqueness(config: OrchestratorConfig): void {
|
|||
/** Apply default reactions */
|
||||
function applyDefaultReactions(config: OrchestratorConfig): OrchestratorConfig {
|
||||
const defaults: Record<string, (typeof config.reactions)[string]> = {
|
||||
"pr-closed": {
|
||||
auto: true,
|
||||
action: "notify",
|
||||
priority: "action",
|
||||
message:
|
||||
"A PR was closed without merging. Decide whether to learn from the closure, resume the work, or terminate the session.",
|
||||
},
|
||||
"ci-failed": {
|
||||
auto: true,
|
||||
action: "send-to-agent",
|
||||
|
|
|
|||
|
|
@ -158,6 +158,8 @@ function prStateToEventType(
|
|||
/** Map event type to reaction config key. */
|
||||
function eventToReactionKey(eventType: EventType): string | null {
|
||||
switch (eventType) {
|
||||
case "pr.closed":
|
||||
return "pr-closed";
|
||||
case "ci.failing":
|
||||
return "ci-failed";
|
||||
case "review.changes_requested":
|
||||
|
|
@ -1588,18 +1590,33 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
|
|||
|
||||
const prEventType = prStateToEventType(previousPRState, session.lifecycle.pr.state);
|
||||
if (prEventType) {
|
||||
const prEvent = createEvent(prEventType, {
|
||||
sessionId: session.id,
|
||||
projectId: session.projectId,
|
||||
message: `${session.id}: PR ${previousPRState} → ${session.lifecycle.pr.state}`,
|
||||
data: {
|
||||
oldPRState: previousPRState,
|
||||
newPRState: session.lifecycle.pr.state,
|
||||
prNumber: session.lifecycle.pr.number,
|
||||
prUrl: session.lifecycle.pr.url,
|
||||
},
|
||||
});
|
||||
await notifyHuman(prEvent, inferPriority(prEventType));
|
||||
let reactionHandledNotify = false;
|
||||
const reactionKey = eventToReactionKey(prEventType);
|
||||
|
||||
if (reactionKey) {
|
||||
const reactionConfig = getReactionConfigForSession(session, reactionKey);
|
||||
if (reactionConfig && reactionConfig.action) {
|
||||
if (reactionConfig.auto !== false || reactionConfig.action === "notify") {
|
||||
await executeReaction(session.id, session.projectId, reactionKey, reactionConfig);
|
||||
reactionHandledNotify = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!reactionHandledNotify) {
|
||||
const prEvent = createEvent(prEventType, {
|
||||
sessionId: session.id,
|
||||
projectId: session.projectId,
|
||||
message: `${session.id}: PR ${previousPRState} → ${session.lifecycle.pr.state}`,
|
||||
data: {
|
||||
oldPRState: previousPRState,
|
||||
newPRState: session.lifecycle.pr.state,
|
||||
prNumber: session.lifecycle.pr.number,
|
||||
prUrl: session.lifecycle.pr.url,
|
||||
},
|
||||
});
|
||||
await notifyHuman(prEvent, inferPriority(prEventType));
|
||||
}
|
||||
}
|
||||
|
||||
// Pin first quality summary for title stability
|
||||
|
|
|
|||
|
|
@ -252,6 +252,23 @@ export function deriveLegacyStatus(
|
|||
lifecycle: CanonicalSessionLifecycle,
|
||||
_previousStatus: SessionStatus = "working",
|
||||
): SessionStatus {
|
||||
switch (lifecycle.session.state) {
|
||||
case "not_started":
|
||||
return "spawning";
|
||||
case "needs_input":
|
||||
return "needs_input";
|
||||
case "stuck":
|
||||
return "stuck";
|
||||
case "done":
|
||||
return "done";
|
||||
case "terminated":
|
||||
return "terminated";
|
||||
case "detecting":
|
||||
return "detecting";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (lifecycle.pr.state === "merged") {
|
||||
return "merged";
|
||||
}
|
||||
|
|
@ -265,20 +282,8 @@ export function deriveLegacyStatus(
|
|||
}
|
||||
|
||||
switch (lifecycle.session.state) {
|
||||
case "not_started":
|
||||
return "spawning";
|
||||
case "needs_input":
|
||||
return "needs_input";
|
||||
case "stuck":
|
||||
return "stuck";
|
||||
case "done":
|
||||
return "done";
|
||||
case "terminated":
|
||||
return "terminated";
|
||||
case "idle":
|
||||
return "idle";
|
||||
case "detecting":
|
||||
return "detecting";
|
||||
case "working":
|
||||
return "working";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue