From 048ae909fdc94d333b30f64d189fb092f9ade4a8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 19 Jul 2026 12:34:55 -0700 Subject: [PATCH] fix(core): restore PostgreSQL lifecycle activity logging (#2344) ## Summary PostgreSQL-backed projects now record automatic TaskStore lifecycle activity, including ordinary task creation and movement. Backend initialization previously returned before registering the lifecycle listeners, while features that called `recordActivity()` directly could make the log appear only partially affected. The fix wires the backend-agnostic listeners without changing SQLite initialization behavior. ## Validation - PostgreSQL activity parity test verifies all six listener surfaces and persisted `task:created` and `task:moved` entries. - `pnpm lint` - `pnpm --filter @fusion/core typecheck` - `pnpm check:changesets --strict` - `pnpm verify:fast` ## Summary by CodeRabbit * **Bug Fixes** * Restored automatic task lifecycle entries in PostgreSQL activity logs by ensuring task lifecycle activity listeners are wired during PostgreSQL backend initialization. * Activity logs now reliably include task creation and movement events (with transition details) immediately after backend startup. * **Tests** * Added a PostgreSQL activity-log parity test to verify listeners are connected and that activity logs reflect `task:created` and `task:moved` as expected. --- .changeset/fix-postgres-activity-lifecycle.md | 7 ++++ .../postgres/activity-log-parity.pg.test.ts | 38 +++++++++++++++++++ packages/core/src/task-store/lifecycle-ops.ts | 12 +++--- 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-postgres-activity-lifecycle.md 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; }