diff --git a/.changeset/fix-postgres-activity-lifecycle.md b/.changeset/fix-postgres-activity-lifecycle.md new file mode 100644 index 0000000000..c0864d953d --- /dev/null +++ b/.changeset/fix-postgres-activity-lifecycle.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore automatic task lifecycle entries in PostgreSQL activity logs. +category: fix +dev: Registers TaskStore activity listeners during PostgreSQL backend initialization. diff --git a/packages/core/src/__tests__/postgres/activity-log-parity.pg.test.ts b/packages/core/src/__tests__/postgres/activity-log-parity.pg.test.ts index b3260bf86f..839cbb3bfa 100644 --- a/packages/core/src/__tests__/postgres/activity-log-parity.pg.test.ts +++ b/packages/core/src/__tests__/postgres/activity-log-parity.pg.test.ts @@ -23,6 +23,44 @@ pgDescribe("activity log parity (PostgreSQL)", () => { afterEach(h.afterEach); afterAll(h.afterAll); + it("records TaskStore lifecycle events after backend initialization", async () => { + const store = h.store(); + + expect(store.activityListenersWired).toBe(true); + for (const event of [ + "task:created", + "task:moved", + "task:merged", + "task:updated", + "task:deleted", + "settings:updated", + ] as const) { + expect(store.listenerCount(event), `${event} activity listener`).toBeGreaterThan(0); + } + + const task = await store.createTask({ + title: "Backend lifecycle activity", + description: "Verify PostgreSQL lifecycle activity logging", + }); + await store.moveTask(task.id, "todo", { moveSource: "user" }); + + await vi.waitFor(async () => { + const entries = await store.getActivityLog({ limit: 10 }); + expect(entries).toEqual(expect.arrayContaining([ + expect.objectContaining({ + type: "task:created", + taskId: task.id, + taskTitle: task.title, + }), + expect.objectContaining({ + type: "task:moved", + taskId: task.id, + metadata: { from: "triage", to: "todo" }, + }), + ])); + }); + }); + it("keeps failed writes best-effort so audit storage cannot break product operations", async () => { const layer = h.layer(); const insert = vi.spyOn(layer.db, "insert").mockImplementation(() => { diff --git a/packages/core/src/task-store/lifecycle-ops.ts b/packages/core/src/task-store/lifecycle-ops.ts index 5b5109d9a0..bf1257ca2b 100644 --- a/packages/core/src/task-store/lifecycle-ops.ts +++ b/packages/core/src/task-store/lifecycle-ops.ts @@ -47,12 +47,9 @@ export async function initImpl(store: TaskStore): Promise { // In backend mode (an AsyncDataLayer was injected), TaskStore skips ALL // SQLite construction and the SQLite-specific startup reconciliations // (corruption guard, legacy file migration, agent-log file migration, - // schema-version re-init, orphaned task-dir reconcile, activity-log - // listener wiring that reads from SQLite, etc.). The PostgreSQL schema - // baseline is applied by the startup factory before constructing the - // store, and the async equivalents of these reconciliations are wired by - // the runtime-*-async features. init() in backend mode performs only the - // backend-agnostic setup (mkdir, trait-hook registration) above and returns. + // schema-version re-init, orphaned task-dir reconcile, etc.). The PostgreSQL + // schema baseline is applied by the startup factory before constructing the + // store. Backend-safe startup work is performed explicitly in the branch below. // // When the async layer is ABSENT, the entire block below runs exactly as // before — byte-identical to the pre-migration SQLite path. @@ -81,6 +78,9 @@ export async function initImpl(store: TaskStore): Promise { async store API (listTasks/updateTask), so it is PG-safe. */ await adoptLegacyTaskRowsOnOpen(store); + // Lifecycle listeners are backend-agnostic: recordActivity() routes their + // best-effort writes through the injected PostgreSQL data layer. + store.setupActivityLogListeners(); return; }