Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1138,8 +1138,8 @@ export async function createAdeRuntime(args: {
// `pr.listQueueStates` call over the local runtime fails with "is not
// callable". Mirror the desktop main-process wiring (see main.ts) so the PRs
// tab loads against the local runtime.
// Fan-out for the push publisher: PR merge-ready / checks-failing notifications
// are bridged here so the publisher never has to poll GitHub itself. Populated
// Fan-out for the push publisher: PR lifecycle/status notifications are
// bridged here so the publisher never has to poll GitHub itself. Populated
// by pushPublisherService.start() (declared below), so it stays empty and inert
// when push publishing is not running.
const pushPrNotificationSubscribers = new Set<(notification: PushPrNotification) => void>();
Expand Down
76 changes: 76 additions & 0 deletions apps/ade-cli/src/services/push/pushPublisherService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
parseHhMm,
shouldDeliverAlertForPrefs,
type AgentRunState,
type PushPrNotification,
} from "./pushPublisherService";

function run(overrides: Partial<AgentRunState>): AgentRunState {
Expand Down Expand Up @@ -226,6 +227,81 @@ describe("createPushPublisherService flush", () => {
publisher.dispose();
});

it("publishes PR lifecycle alerts into the aggregate Live Activity", async () => {
const { publisher, publish } = makeHarness();
let firstPrCb: (event: PushPrNotification) => void = () => {
throw new Error("first PR notification source was not attached");
};
let secondPrCb: (event: PushPrNotification) => void = () => {
throw new Error("second PR notification source was not attached");
};
publisher.attachSources("project-a", {
subscribePrNotifications: (cb) => {
firstPrCb = cb;
return () => {};
},
resolveLaneName: (laneId: string) => laneId === "lane-42" ? "Mobile PR lane" : laneId,
});
publisher.attachSources("project-b", {
subscribePrNotifications: (cb) => {
secondPrCb = cb;
return () => {};
},
resolveLaneName: (laneId: string) => laneId === "lane-other" ? "Other repo lane" : laneId,
});
await publisher.start();

firstPrCb({ kind: "merged", prNumber: 42, prTitle: "Ship mobile PR view", laneId: "lane-42" });
await vi.advanceTimersByTimeAsync(2_500);

const payload = publish.mock.calls[0][0];
expect(payload.notifications[0]).toMatchObject({
title: "PR #42 merged",
body: "Ship mobile PR view",
deepLink: "ade://pr/42",
threadId: "pr:project-a:42",
});
expect(payload.liveActivity[0].contentState.prs[0]).toMatchObject({
id: "pr:project-a:42",
prNumber: 42,
title: "Ship mobile PR view",
phase: "merged",
lane: "Mobile PR lane",
});
expect(payload.liveActivity[0].phase).toBe("running");

secondPrCb({ kind: "checks_failing", prNumber: 42, prTitle: "Same number, other repo", laneId: "lane-other" });
await vi.advanceTimersByTimeAsync(2_500);

const updatePayload = publish.mock.calls.at(-1)?.[0];
expect(updatePayload?.liveActivity[0].contentState.prs).toMatchObject([
{
id: "pr:project-b:42",
prNumber: 42,
title: "Same number, other repo",
phase: "checks_failing",
lane: "Other repo lane",
},
{
id: "pr:project-a:42",
prNumber: 42,
title: "Ship mobile PR view",
phase: "merged",
lane: "Mobile PR lane",
},
]);

await vi.advanceTimersByTimeAsync(45 * 60 * 1000 + 1_000);
const endPayload = publish.mock.calls.at(-1)?.[0];
expect(endPayload.liveActivity[0]).toMatchObject({
event: "end",
phase: "terminal",
});
expect(endPayload.liveActivity[0].contentState.prs).toEqual([]);

publisher.dispose();
});

it("carries actionable fields: category + sessionId/itemId on the alert, itemId on the waiting LA row, badge count", async () => {
const { publisher, publish, emit } = makeHarness();
await publisher.start();
Expand Down
Loading