feat(FN-4783): complete Steps 4-5 — provider rendering and engine wiring
Fusion-Task-Id: FN-4783 Fusion-Task-Lineage: 7a02897b-e303-4947-86b2-3dd7a343f653
This commit is contained in:
committed by
gsxdsm
parent
b48f8f1fac
commit
5e159368dc
@@ -47,6 +47,7 @@ describe("NtfyNotificationProvider", () => {
|
||||
["message:agent-to-user", "New message from Triage Bot", "Triage Bot → you: preview text", "high"],
|
||||
["message:agent-to-agent", "Triage Bot → Executor Bot", "Triage Bot messaged Executor Bot: preview text", "default"],
|
||||
["message:room", "#Incident Room — Triage Bot", "Triage Bot in #Incident Room: preview text", "default"],
|
||||
["oauth-token-expired", "OAuth token expired", "Your OpenAI Codex OAuth token has expired", "high"],
|
||||
])("maps %s event correctly", async (event, expectedTitle, messagePart, priority) => {
|
||||
await provider.sendNotification(event as any, {
|
||||
taskId: "FN-1",
|
||||
@@ -63,6 +64,8 @@ describe("NtfyNotificationProvider", () => {
|
||||
messageId: "msg-1",
|
||||
roomId: "room-1",
|
||||
roomName: "Incident Room",
|
||||
providerId: "openai-codex",
|
||||
providerName: "OpenAI Codex",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,6 +96,7 @@ describe("NtfyNotificationProvider", () => {
|
||||
expect(provider.isEventSupported("message:agent-to-user" as any)).toBe(true);
|
||||
expect(provider.isEventSupported("message:agent-to-agent" as any)).toBe(true);
|
||||
expect(provider.isEventSupported("message:room" as any)).toBe(true);
|
||||
expect(provider.isEventSupported("oauth-token-expired" as any)).toBe(true);
|
||||
expect(provider.isEventSupported("custom-event" as any)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ProjectEngine } from "../project-engine.js";
|
||||
import { runtimeLog } from "../logger.js";
|
||||
import { TunnelProcessManager } from "../remote-access/tunnel-process-manager.js";
|
||||
import { NtfyNotifier } from "../notifier.js";
|
||||
import { NotificationService } from "../notification/index.js";
|
||||
import { NotificationService, OAuthExpiryMonitor } from "../notification/index.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
syncInsightExtractionAutomation: vi.fn(),
|
||||
@@ -25,6 +25,8 @@ const mocks = vi.hoisted(() => ({
|
||||
notifierNotifyGridlock: vi.fn(),
|
||||
notificationServiceStart: vi.fn(async () => undefined),
|
||||
notificationServiceStop: vi.fn(),
|
||||
oauthExpiryMonitorStart: vi.fn(async () => undefined),
|
||||
oauthExpiryMonitorStop: vi.fn(),
|
||||
runtimeConfigurePrMonitoring: vi.fn(),
|
||||
prHandlerCreateFollowUpTask: vi.fn(async () => undefined),
|
||||
}));
|
||||
@@ -92,6 +94,18 @@ vi.mock("../notification/index.js", () => ({
|
||||
start: mocks.notificationServiceStart,
|
||||
stop: mocks.notificationServiceStop,
|
||||
})),
|
||||
OAuthExpiryMonitor: vi.fn().mockImplementation(() => ({
|
||||
start: mocks.oauthExpiryMonitorStart,
|
||||
stop: mocks.oauthExpiryMonitorStop,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../auth-storage.js", () => ({
|
||||
createFusionAuthStorage: vi.fn(() => ({
|
||||
reload: vi.fn(),
|
||||
getOAuthProviders: vi.fn(() => []),
|
||||
get: vi.fn(() => undefined),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../runtimes/in-process-runtime.js", () => ({
|
||||
@@ -244,6 +258,8 @@ beforeEach(() => {
|
||||
mocks.notifierNotifyGridlock.mockClear();
|
||||
mocks.notificationServiceStart.mockClear();
|
||||
mocks.notificationServiceStop.mockClear();
|
||||
mocks.oauthExpiryMonitorStart.mockClear();
|
||||
mocks.oauthExpiryMonitorStop.mockClear();
|
||||
|
||||
mocks.execFile.mockImplementation((
|
||||
_file: string,
|
||||
@@ -277,14 +293,17 @@ describe("ProjectEngine notification ownership wiring", () => {
|
||||
await engine.start();
|
||||
|
||||
expect(NotificationService).toHaveBeenCalledTimes(1);
|
||||
expect(OAuthExpiryMonitor).toHaveBeenCalledTimes(1);
|
||||
expect(NtfyNotifier).toHaveBeenCalledTimes(1);
|
||||
const notifierCtorArgs = vi.mocked(NtfyNotifier).mock.calls[0];
|
||||
expect(notifierCtorArgs?.[2]).toBe(vi.mocked(NotificationService).mock.results[0]?.value);
|
||||
|
||||
expect(mocks.notificationServiceStart).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.oauthExpiryMonitorStart).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notifierStart).toHaveBeenCalledTimes(1);
|
||||
|
||||
await engine.stop();
|
||||
expect(mocks.oauthExpiryMonitorStop).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notificationServiceStop).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notifierStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -298,12 +317,26 @@ describe("ProjectEngine notification ownership wiring", () => {
|
||||
// Root cause guard: if ProjectEngine.start is called more than once, it should not
|
||||
// wire a second NotificationService/NtfyNotifier pair for the same store.
|
||||
expect(NotificationService).toHaveBeenCalledTimes(1);
|
||||
expect(OAuthExpiryMonitor).toHaveBeenCalledTimes(1);
|
||||
expect(NtfyNotifier).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notificationServiceStart).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.oauthExpiryMonitorStart).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notifierStart).toHaveBeenCalledTimes(1);
|
||||
|
||||
await engine.stop();
|
||||
});
|
||||
|
||||
it("does not create OAuth expiry monitor when notifier is skipped", async () => {
|
||||
const engine = createEngine({ skipNotifier: true, projectId: "proj_for_notifier" });
|
||||
|
||||
await engine.start();
|
||||
|
||||
expect(NotificationService).not.toHaveBeenCalled();
|
||||
expect(OAuthExpiryMonitor).not.toHaveBeenCalled();
|
||||
expect(NtfyNotifier).not.toHaveBeenCalled();
|
||||
|
||||
await engine.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectEngine accessors", () => {
|
||||
|
||||
@@ -40,7 +40,8 @@ type SupportedNtfyEvent =
|
||||
| "fallback-used"
|
||||
| "message:agent-to-user"
|
||||
| "message:agent-to-agent"
|
||||
| "message:room";
|
||||
| "message:room"
|
||||
| "oauth-token-expired";
|
||||
|
||||
const SUPPORTED_EVENTS = new Set<SupportedNtfyEvent>([
|
||||
"in-review",
|
||||
@@ -53,6 +54,7 @@ const SUPPORTED_EVENTS = new Set<SupportedNtfyEvent>([
|
||||
"message:agent-to-user",
|
||||
"message:agent-to-agent",
|
||||
"message:room",
|
||||
"oauth-token-expired",
|
||||
]);
|
||||
|
||||
export function resolveParticipantLabel(
|
||||
@@ -169,13 +171,20 @@ export class NtfyNotificationProvider implements NotificationProvider {
|
||||
messageId,
|
||||
view: "rooms",
|
||||
})
|
||||
: buildNtfyClickUrl({
|
||||
dashboardHost: this.config.dashboardHost,
|
||||
projectId: this.config.projectId,
|
||||
taskId: payload.taskId,
|
||||
messageId,
|
||||
view: "mailbox",
|
||||
});
|
||||
: event === "oauth-token-expired"
|
||||
? undefined
|
||||
: buildNtfyClickUrl({
|
||||
dashboardHost: this.config.dashboardHost,
|
||||
projectId: this.config.projectId,
|
||||
taskId: payload.taskId,
|
||||
messageId,
|
||||
view: "mailbox",
|
||||
});
|
||||
|
||||
const providerId = typeof payload.metadata?.providerId === "string" ? payload.metadata.providerId : "provider";
|
||||
const providerName = typeof payload.metadata?.providerName === "string"
|
||||
? payload.metadata.providerName
|
||||
: providerId;
|
||||
|
||||
const contentByEvent: Record<SupportedNtfyEvent, { title: string; message: string; priority: "default" | "high" }> = {
|
||||
"in-review": {
|
||||
@@ -228,6 +237,11 @@ export class NtfyNotificationProvider implements NotificationProvider {
|
||||
message: `${roomSenderLabel} in #${roomLabel}: ${preview}`,
|
||||
priority: "default",
|
||||
},
|
||||
"oauth-token-expired": {
|
||||
title: "OAuth token expired",
|
||||
message: `Your ${providerName} OAuth token has expired — please re-authenticate`,
|
||||
priority: "high",
|
||||
},
|
||||
};
|
||||
|
||||
const content = contentByEvent[event as SupportedNtfyEvent];
|
||||
|
||||
@@ -191,6 +191,11 @@ export class WebhookNotificationProvider implements NotificationProvider {
|
||||
const preview = typeof payload.metadata?.preview === "string" ? payload.metadata.preview : "(no preview)";
|
||||
return `In #${roomName}: ${senderLabel}: ${preview}`;
|
||||
}
|
||||
case "oauth-token-expired": {
|
||||
const providerId = typeof payload.metadata?.providerId === "string" ? payload.metadata.providerId : "provider";
|
||||
const providerName = typeof payload.metadata?.providerName === "string" ? payload.metadata.providerName : providerId;
|
||||
return `Your ${providerName} OAuth token has expired — please re-authenticate`;
|
||||
}
|
||||
default:
|
||||
return `Event "${event}" for task ${identifier}`;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ import type { ProjectRuntimeConfig } from "./project-runtime.js";
|
||||
import { PrMonitor } from "./pr-monitor.js";
|
||||
import { PrCommentHandler } from "./pr-comment-handler.js";
|
||||
import { NtfyNotifier } from "./notifier.js";
|
||||
import { NotificationService } from "./notification/index.js";
|
||||
import { NotificationService, OAuthExpiryMonitor } from "./notification/index.js";
|
||||
import type { NotificationChatStore } from "./notification/notification-service.js";
|
||||
import { GridlockDetector } from "./gridlock-detector.js";
|
||||
import { createFusionAuthStorage } from "./auth-storage.js";
|
||||
import { CronRunner, createAiPromptExecutor } from "./cron-runner.js";
|
||||
import type { RoutineRunner } from "./routine-runner.js";
|
||||
import { aiMergeTask, sweepStaleAutostashes, VerificationError } from "./merger.js";
|
||||
@@ -152,6 +153,7 @@ export class ProjectEngine {
|
||||
private prCommentHandler?: PrCommentHandler;
|
||||
private notifier?: NtfyNotifier;
|
||||
private notificationService?: NotificationService;
|
||||
private oauthExpiryMonitor?: OAuthExpiryMonitor;
|
||||
private gridlockDetector?: GridlockDetector;
|
||||
private cronRunner?: CronRunner;
|
||||
private automationStore?: AutomationStoreType;
|
||||
@@ -327,6 +329,11 @@ export class ProjectEngine {
|
||||
agentNameResolver,
|
||||
});
|
||||
await this.notificationService.start();
|
||||
this.oauthExpiryMonitor = new OAuthExpiryMonitor({
|
||||
authStorage: createFusionAuthStorage(),
|
||||
notificationService: this.notificationService,
|
||||
});
|
||||
await this.oauthExpiryMonitor.start();
|
||||
|
||||
// Backward-compatibility shim for gridlock notifications.
|
||||
this.notifier = new NtfyNotifier(
|
||||
@@ -546,6 +553,7 @@ export class ProjectEngine {
|
||||
}
|
||||
|
||||
// Stop auxiliary subsystems
|
||||
this.oauthExpiryMonitor?.stop();
|
||||
this.notificationService?.stop();
|
||||
this.notifier?.stop();
|
||||
this.gridlockDetector?.stop();
|
||||
|
||||
Reference in New Issue
Block a user