feat(FN-3584): add memory file markdown preview to agent detail and log vie
This merge introduces a memory file markdown preview feature (FN-3584) with corresponding documentation, refines the AgentDetailView and AgentLogViewer components in the dashboard, and adds defensive collision handling for worktree operations during manual task moves (FN-3583). Fusion-Task-Id: FN-3584
This commit is contained in:
@@ -128,4 +128,36 @@ describe("NotificationService", () => {
|
||||
expect(initSpy).not.toHaveBeenCalled();
|
||||
initSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("duplicates merged dispatch when multiple NotificationService instances subscribe to the same store", async () => {
|
||||
const store = createStore({ ntfyEnabled: true, ntfyTopic: "topic" });
|
||||
const sendNotification = vi.fn(async () => ({ success: true, providerId: "mock" }));
|
||||
const provider: NotificationProvider = {
|
||||
getProviderId: () => "mock",
|
||||
isEventSupported: () => true,
|
||||
sendNotification,
|
||||
};
|
||||
|
||||
const first = new NotificationService(store as any);
|
||||
const second = new NotificationService(store as any);
|
||||
first.registerProvider(provider);
|
||||
second.registerProvider(provider);
|
||||
await first.start();
|
||||
await second.start();
|
||||
|
||||
// Confirms duplication is from duplicate listener graphs, not duplicate task:merged payloads.
|
||||
store.emit("task:merged", {
|
||||
task: task(),
|
||||
branch: "fusion/fn-1",
|
||||
merged: true,
|
||||
worktreeRemoved: true,
|
||||
branchDeleted: true,
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(sendNotification).toHaveBeenCalledTimes(2);
|
||||
|
||||
await first.stop();
|
||||
await second.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1093,7 +1093,7 @@ describe("NtfyNotifier", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("emits a single merged notification when notifier shares an already-started NotificationService", async () => {
|
||||
it("emits a single merged notification when notifier shares the same already-started NotificationService (ProjectEngine wiring)", async () => {
|
||||
const sharedService = new NotificationService(store, { projectId: "proj-1" });
|
||||
await sharedService.start();
|
||||
|
||||
|
||||
@@ -288,6 +288,22 @@ describe("ProjectEngine notification ownership wiring", () => {
|
||||
expect(mocks.notificationServiceStop).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notifierStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not recreate notification listeners on repeated start calls, preventing duplicate merged delivery", async () => {
|
||||
const engine = createEngine({ skipNotifier: false, projectId: "proj_for_notifier" });
|
||||
|
||||
await engine.start();
|
||||
await engine.start();
|
||||
|
||||
// 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(NtfyNotifier).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notificationServiceStart).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.notifierStart).toHaveBeenCalledTimes(1);
|
||||
|
||||
await engine.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectEngine PR monitoring wiring", () => {
|
||||
|
||||
@@ -139,6 +139,7 @@ export interface ProjectEngineOptions {
|
||||
*/
|
||||
export class ProjectEngine {
|
||||
private runtime: InProcessRuntime;
|
||||
private started = false;
|
||||
private prMonitor?: PrMonitor;
|
||||
private prCommentHandler?: PrCommentHandler;
|
||||
private notifier?: NtfyNotifier;
|
||||
@@ -243,6 +244,10 @@ export class ProjectEngine {
|
||||
* Start the engine: initialize the runtime and all auxiliary subsystems.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Start the core runtime (TaskStore, Scheduler, Executor, Triage, etc.)
|
||||
await this.runtime.start();
|
||||
|
||||
@@ -422,6 +427,7 @@ export class ProjectEngine {
|
||||
// 8. Start periodic merge retry sweep
|
||||
this.scheduleMergeRetry(store);
|
||||
|
||||
this.started = true;
|
||||
runtimeLog.log(`ProjectEngine started for ${this.config.projectId}`);
|
||||
}
|
||||
|
||||
@@ -433,6 +439,10 @@ export class ProjectEngine {
|
||||
* promptly without continuing git/verification work after shutdown starts.
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shuttingDown = true;
|
||||
|
||||
// Stop merge retry timer
|
||||
@@ -518,6 +528,8 @@ export class ProjectEngine {
|
||||
// Stop the core runtime (Triage, Scheduler, Executor, etc.)
|
||||
await this.runtime.stop();
|
||||
|
||||
this.started = false;
|
||||
this.shuttingDown = false;
|
||||
runtimeLog.log(`ProjectEngine stopped for ${this.config.projectId}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user