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:
Fusion
2026-05-06 07:45:56 -07:00
committed by gsxdsm
parent 9dd8445c96
commit 270823dcc9
9 changed files with 217 additions and 165 deletions

View File

@@ -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();
});
});

View File

@@ -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();

View File

@@ -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", () => {

View File

@@ -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}`);
}