FN-5703: forward task:deleted events through in-process runtime
Ensure engine-side task deletes propagate through all runtime transports so GitHub tracking cleanup runs consistently. - extend IPC protocol and child-process runtime/worker plumbing to carry `task:deleted` events - forward `task:deleted` from in-process runtime through project runtime and project manager dispatch - add regression coverage for in-process runtime forwarding and GitHub tracking delete handling Files changed: .../src/__tests__/github-tracking-delete.test.ts | 29 ++++++++++++++++++ .../src/__tests__/in-process-runtime.test.ts | 34 +++++++++++++++++++++- packages/engine/src/ipc/ipc-protocol.ts | 14 ++++++++- packages/engine/src/project-manager.ts | 7 +++++ packages/engine/src/project-runtime.ts | 4 ++- .../engine/src/runtimes/child-process-runtime.ts | 6 ++++ .../engine/src/runtimes/child-process-worker.ts | 14 +++++++-- packages/engine/src/runtimes/in-process-runtime.ts | 10 ++++++- 8 files changed, 111 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-5703 Fusion-Task-Lineage: 1406838b-5abf-4b86-8161-c2dd59f88a9b
This commit is contained in:
@@ -133,6 +133,35 @@ describe("github tracking delete flow", () => {
|
|||||||
expect(mockSetIssueState).toHaveBeenCalledWith("octocat", "hello-world", 7, "closed", "not_planned");
|
expect(mockSetIssueState).toHaveBeenCalledWith("octocat", "hello-world", 7, "closed", "not_planned");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses task:deleted githubIssueAction=auto metadata to close linked issue", async () => {
|
||||||
|
const handleTaskDeletedSpy = vi.spyOn(stateService as any, "handleTaskDeleted");
|
||||||
|
const task = await store.createTask({
|
||||||
|
description: "delete tracked task with default auto metadata",
|
||||||
|
githubTracking: { enabled: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.linkGithubIssue(task.id, {
|
||||||
|
owner: "octocat",
|
||||||
|
repo: "hello-world",
|
||||||
|
number: 70,
|
||||||
|
url: "https://github.com/octocat/hello-world/issues/70",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeAction = waitForGithubIssueAction(
|
||||||
|
store,
|
||||||
|
(payload) => payload.taskId === task.id && payload.action === "close" && payload.outcome === "success",
|
||||||
|
{ timeoutMessage: `Timed out waiting for auto close action for deleted task ${task.id}` },
|
||||||
|
);
|
||||||
|
|
||||||
|
await store.deleteTask(task.id, { githubIssueAction: "auto" });
|
||||||
|
await closeAction;
|
||||||
|
|
||||||
|
expect(handleTaskDeletedSpy).toHaveBeenCalled();
|
||||||
|
expect(handleTaskDeletedSpy).toHaveBeenCalledWith(store, expect.objectContaining({ id: task.id }), { githubIssueAction: "auto" });
|
||||||
|
expect(mockSetIssueState).toHaveBeenCalledWith("octocat", "hello-world", 70, "closed", "not_planned");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not call GitHub when deleting a task with tracking disabled", async () => {
|
it("does not call GitHub when deleting a task with tracking disabled", async () => {
|
||||||
const task = await store.createTask({
|
const task = await store.createTask({
|
||||||
description: "delete untracked task",
|
description: "delete untracked task",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
import { InProcessRuntime } from "../runtimes/in-process-runtime.js";
|
||||||
|
|
||||||
describe("InProcessRuntime onStart duplicate guard", () => {
|
describe("InProcessRuntime onStart duplicate guard", () => {
|
||||||
it("contains a taskAgentMap guard before creating task-worker agents", () => {
|
it("contains a taskAgentMap guard before creating task-worker agents", () => {
|
||||||
@@ -21,4 +23,34 @@ describe("InProcessRuntime onStart duplicate guard", () => {
|
|||||||
const source = readFileSync(join(process.cwd(), "src/runtimes/in-process-runtime.ts"), "utf-8");
|
const source = readFileSync(join(process.cwd(), "src/runtimes/in-process-runtime.ts"), "utf-8");
|
||||||
expect(source).toContain("activeMissionAutopilot.recoverMissions(activeMissionStore)");
|
expect(source).toContain("activeMissionAutopilot.recoverMissions(activeMissionStore)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards task:deleted events with and without githubIssueAction metadata", () => {
|
||||||
|
const runtime = new InProcessRuntime(
|
||||||
|
{
|
||||||
|
projectId: "proj-test",
|
||||||
|
workingDirectory: process.cwd(),
|
||||||
|
isolationMode: "in-process",
|
||||||
|
maxConcurrent: 1,
|
||||||
|
maxWorktrees: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getGlobalConcurrencyState: vi.fn(),
|
||||||
|
recordTaskCompletion: vi.fn(),
|
||||||
|
} as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
const taskStore = new EventEmitter();
|
||||||
|
const emitSpy = vi.spyOn(runtime, "emit");
|
||||||
|
(runtime as any).taskStore = taskStore;
|
||||||
|
(runtime as any).setupEventForwarding();
|
||||||
|
|
||||||
|
const task = { id: "FN-1", title: "task" };
|
||||||
|
const meta = { githubIssueAction: "auto" };
|
||||||
|
|
||||||
|
taskStore.emit("task:deleted", task, meta);
|
||||||
|
taskStore.emit("task:deleted", task);
|
||||||
|
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith("task:deleted", task, meta);
|
||||||
|
expect(emitSpy).toHaveBeenCalledWith("task:deleted", task, undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RuntimeStatus, ProjectRuntimeConfig } from "../project-runtime.js";
|
import type { RuntimeStatus, ProjectRuntimeConfig } from "../project-runtime.js";
|
||||||
import type { Task } from "@fusion/core";
|
import type { GithubIssueAction, Task } from "@fusion/core";
|
||||||
|
|
||||||
// ── Base Message Types ────────────────────────────────────────────────────
|
// ── Base Message Types ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -121,6 +121,8 @@ export const TASK_CREATED = "TASK_CREATED" as const;
|
|||||||
export const TASK_MOVED = "TASK_MOVED" as const;
|
export const TASK_MOVED = "TASK_MOVED" as const;
|
||||||
/** Event type: Task updated */
|
/** Event type: Task updated */
|
||||||
export const TASK_UPDATED = "TASK_UPDATED" as const;
|
export const TASK_UPDATED = "TASK_UPDATED" as const;
|
||||||
|
/** Event type: Task deleted */
|
||||||
|
export const TASK_DELETED = "TASK_DELETED" as const;
|
||||||
/** Event type: Runtime error */
|
/** Event type: Runtime error */
|
||||||
export const ERROR_EVENT = "ERROR_EVENT" as const;
|
export const ERROR_EVENT = "ERROR_EVENT" as const;
|
||||||
/** Event type: Health status changed */
|
/** Event type: Health status changed */
|
||||||
@@ -133,6 +135,7 @@ export type IpcEventType =
|
|||||||
| typeof TASK_CREATED
|
| typeof TASK_CREATED
|
||||||
| typeof TASK_MOVED
|
| typeof TASK_MOVED
|
||||||
| typeof TASK_UPDATED
|
| typeof TASK_UPDATED
|
||||||
|
| typeof TASK_DELETED
|
||||||
| typeof ERROR_EVENT
|
| typeof ERROR_EVENT
|
||||||
| typeof HEALTH_CHANGED;
|
| typeof HEALTH_CHANGED;
|
||||||
|
|
||||||
@@ -159,6 +162,14 @@ export interface TaskUpdatedPayload {
|
|||||||
task: Task;
|
task: Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for TASK_DELETED event.
|
||||||
|
*/
|
||||||
|
export interface TaskDeletedPayload {
|
||||||
|
task: Task;
|
||||||
|
meta?: { githubIssueAction?: GithubIssueAction };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payload for ERROR event.
|
* Payload for ERROR event.
|
||||||
*/
|
*/
|
||||||
@@ -209,6 +220,7 @@ export function isIpcEvent(message: IpcMessage): boolean {
|
|||||||
TASK_CREATED,
|
TASK_CREATED,
|
||||||
TASK_MOVED,
|
TASK_MOVED,
|
||||||
TASK_UPDATED,
|
TASK_UPDATED,
|
||||||
|
TASK_DELETED,
|
||||||
ERROR_EVENT,
|
ERROR_EVENT,
|
||||||
HEALTH_CHANGED,
|
HEALTH_CHANGED,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { projectManagerLog } from "./logger.js";
|
|||||||
export interface ProjectManagerEvents {
|
export interface ProjectManagerEvents {
|
||||||
/** Emitted when a task is created in any project */
|
/** Emitted when a task is created in any project */
|
||||||
"task:created": [data: { projectId: string; projectName: string; task: Task }];
|
"task:created": [data: { projectId: string; projectName: string; task: Task }];
|
||||||
|
/** Emitted when a task is deleted in any project */
|
||||||
|
"task:deleted": [data: { projectId: string; projectName: string; task: Task; meta?: { githubIssueAction?: import("@fusion/core").GithubIssueAction } }];
|
||||||
/** Emitted when a task is moved in any project */
|
/** Emitted when a task is moved in any project */
|
||||||
"task:moved": [
|
"task:moved": [
|
||||||
data: {
|
data: {
|
||||||
@@ -397,6 +399,11 @@ export class ProjectManager extends EventEmitter<ProjectManagerEvents> {
|
|||||||
this.emit("task:updated", { projectId, projectName, task });
|
this.emit("task:updated", { projectId, projectName, task });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Forward task:deleted
|
||||||
|
runtime.on("task:deleted", (task: Task, meta?: { githubIssueAction?: import("@fusion/core").GithubIssueAction }) => {
|
||||||
|
this.emit("task:deleted", { projectId, projectName, task, meta });
|
||||||
|
});
|
||||||
|
|
||||||
// Forward errors
|
// Forward errors
|
||||||
runtime.on("error", (error: Error) => {
|
runtime.on("error", (error: Error) => {
|
||||||
this.emit("error", { projectId, projectName, error });
|
this.emit("error", { projectId, projectName, error });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { EventEmitter } from "node:events";
|
import type { EventEmitter } from "node:events";
|
||||||
import type { TaskStore, Task, IsolationMode, ProjectSettings } from "@fusion/core";
|
import type { TaskStore, Task, IsolationMode, ProjectSettings, GithubIssueAction } from "@fusion/core";
|
||||||
import type { Scheduler } from "./scheduler.js";
|
import type { Scheduler } from "./scheduler.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,6 +67,8 @@ export interface ProjectRuntimeEvents {
|
|||||||
"task:moved": [data: { task: Task; from: string; to: string }];
|
"task:moved": [data: { task: Task; from: string; to: string }];
|
||||||
/** Emitted when a task is updated */
|
/** Emitted when a task is updated */
|
||||||
"task:updated": [task: Task];
|
"task:updated": [task: Task];
|
||||||
|
/** Emitted when a task is deleted */
|
||||||
|
"task:deleted": [task: Task, meta?: { githubIssueAction?: GithubIssueAction }];
|
||||||
/** Emitted when a cross-node assignment event is observed */
|
/** Emitted when a cross-node assignment event is observed */
|
||||||
"task:assigned": [data: { taskId: string; agentId: string; assignedAt: string; source?: string }];
|
"task:assigned": [data: { taskId: string; agentId: string; assignedAt: string; source?: string }];
|
||||||
/** Emitted when an error occurs in the runtime */
|
/** Emitted when an error occurs in the runtime */
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ import {
|
|||||||
TASK_CREATED,
|
TASK_CREATED,
|
||||||
TASK_MOVED,
|
TASK_MOVED,
|
||||||
TASK_UPDATED,
|
TASK_UPDATED,
|
||||||
|
TASK_DELETED,
|
||||||
ERROR_EVENT,
|
ERROR_EVENT,
|
||||||
HEALTH_CHANGED,
|
HEALTH_CHANGED,
|
||||||
type TaskCreatedPayload,
|
type TaskCreatedPayload,
|
||||||
type TaskMovedPayload,
|
type TaskMovedPayload,
|
||||||
type TaskUpdatedPayload,
|
type TaskUpdatedPayload,
|
||||||
|
type TaskDeletedPayload,
|
||||||
type ErrorEventPayload,
|
type ErrorEventPayload,
|
||||||
type HealthChangedPayload,
|
type HealthChangedPayload,
|
||||||
} from "../ipc/ipc-protocol.js";
|
} from "../ipc/ipc-protocol.js";
|
||||||
@@ -296,6 +298,10 @@ export class ChildProcessRuntime
|
|||||||
this.emit("task:updated", payload.task);
|
this.emit("task:updated", payload.task);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ipcHost.on(TASK_DELETED, (payload: TaskDeletedPayload) => {
|
||||||
|
this.emit("task:deleted", payload.task, payload.meta);
|
||||||
|
});
|
||||||
|
|
||||||
// Forward error events
|
// Forward error events
|
||||||
this.ipcHost.on(ERROR_EVENT, (payload: ErrorEventPayload) => {
|
this.ipcHost.on(ERROR_EVENT, (payload: ErrorEventPayload) => {
|
||||||
const error = new Error(payload.message);
|
const error = new Error(payload.message);
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import {
|
|||||||
STOP_RUNTIME,
|
STOP_RUNTIME,
|
||||||
GET_STATUS,
|
GET_STATUS,
|
||||||
GET_METRICS,
|
GET_METRICS,
|
||||||
|
TASK_CREATED,
|
||||||
|
TASK_MOVED,
|
||||||
|
TASK_UPDATED,
|
||||||
|
TASK_DELETED,
|
||||||
ERROR_EVENT,
|
ERROR_EVENT,
|
||||||
type StartRuntimePayload,
|
type StartRuntimePayload,
|
||||||
} from "../ipc/ipc-protocol.js";
|
} from "../ipc/ipc-protocol.js";
|
||||||
@@ -76,15 +80,19 @@ ipcWorker.onCommand(START_RUNTIME, async (payload: unknown) => {
|
|||||||
|
|
||||||
// Forward runtime events to host
|
// Forward runtime events to host
|
||||||
runtime.on("task:created", (task) => {
|
runtime.on("task:created", (task) => {
|
||||||
ipcWorker.sendEvent("TASK_CREATED", { task });
|
ipcWorker.sendEvent(TASK_CREATED, { task });
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime.on("task:moved", (data) => {
|
runtime.on("task:moved", (data) => {
|
||||||
ipcWorker.sendEvent("TASK_MOVED", data);
|
ipcWorker.sendEvent(TASK_MOVED, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime.on("task:updated", (task) => {
|
runtime.on("task:updated", (task) => {
|
||||||
ipcWorker.sendEvent("TASK_UPDATED", { task });
|
ipcWorker.sendEvent(TASK_UPDATED, { task });
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.on("task:deleted", (task, meta) => {
|
||||||
|
ipcWorker.sendEvent(TASK_DELETED, { task, meta });
|
||||||
});
|
});
|
||||||
|
|
||||||
runtime.on("error", (error) => {
|
runtime.on("error", (error) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
PluginLoader,
|
PluginLoader,
|
||||||
MessageStore,
|
MessageStore,
|
||||||
RoutineStore,
|
RoutineStore,
|
||||||
|
GithubIssueAction,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import { ChatStore, createCentralDatabase, isEphemeralAgent } from "@fusion/core";
|
import { ChatStore, createCentralDatabase, isEphemeralAgent } from "@fusion/core";
|
||||||
import { Scheduler } from "../scheduler.js";
|
import { Scheduler } from "../scheduler.js";
|
||||||
@@ -1284,7 +1285,8 @@ export class InProcessRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up event forwarding from TaskStore to runtime listeners.
|
* Set up event forwarding from TaskStore to runtime listeners
|
||||||
|
* for task:created, task:moved, task:updated, and task:deleted.
|
||||||
*/
|
*/
|
||||||
private setupEventForwarding(): void {
|
private setupEventForwarding(): void {
|
||||||
// Forward task:created events
|
// Forward task:created events
|
||||||
@@ -1305,6 +1307,12 @@ export class InProcessRuntime
|
|||||||
this.emit("task:updated", task);
|
this.emit("task:updated", task);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Forward task:deleted events
|
||||||
|
this.taskStore.on("task:deleted", (task: Task, meta?: { githubIssueAction?: GithubIssueAction }) => {
|
||||||
|
this.recordActivity();
|
||||||
|
this.emit("task:deleted", task, meta);
|
||||||
|
});
|
||||||
|
|
||||||
runtimeLog.log("Event forwarding setup complete");
|
runtimeLog.log("Event forwarding setup complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user