From 291fabcce8e6b3eefbaa65af6706c6fc15b21478 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 08:41:34 -0700 Subject: [PATCH] FN-8083: preserve GitLab tracking across archive restore Preserve GitLab import tracking metadata throughout TaskStore archive and restore flows. - Store GitLab tracking data in archived task entries and restored tasks - Hydrate GitLab tracking when serializing archive entries - Cover live reads, searches, modified-task listings, and archive restoration in PostgreSQL tests - Add a patch changeset for the tracking persistence fix Files changed: .changeset/fn-8083-gitlab-tracking-hydration.md | 7 +++ .../store-gitlab-tracking-hydration.pg.test.ts | 58 +++++++++++----------- .../core/src/task-store/archive-lifecycle-2.ts | 7 +++ packages/core/src/task-store/serialization.ts | 1 + 4 files changed, 43 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-8083 Fusion-Task-Lineage: 877f320f-11c9-469d-8ea5-b843e95b78d3 Co-authored-by: Fusion (runfusion.ai) --- .../fn-8083-gitlab-tracking-hydration.md | 7 +++ ...store-gitlab-tracking-hydration.pg.test.ts | 62 +++++++++---------- .../src/task-store/archive-lifecycle-2.ts | 7 +++ packages/core/src/task-store/serialization.ts | 1 + 4 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 .changeset/fn-8083-gitlab-tracking-hydration.md diff --git a/.changeset/fn-8083-gitlab-tracking-hydration.md b/.changeset/fn-8083-gitlab-tracking-hydration.md new file mode 100644 index 0000000000..0943b9ba2f --- /dev/null +++ b/.changeset/fn-8083-gitlab-tracking-hydration.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Preserve GitLab import tracking metadata when tasks are read or restored. +category: fix +dev: GitLab tracking now has archive/restore parity with the shared TaskStore mapping. diff --git a/packages/core/src/__tests__/postgres/store-gitlab-tracking-hydration.pg.test.ts b/packages/core/src/__tests__/postgres/store-gitlab-tracking-hydration.pg.test.ts index 21b078d40b..8014f3104d 100644 --- a/packages/core/src/__tests__/postgres/store-gitlab-tracking-hydration.pg.test.ts +++ b/packages/core/src/__tests__/postgres/store-gitlab-tracking-hydration.pg.test.ts @@ -7,10 +7,10 @@ import { } from "../../__test-utils__/pg-test-harness.js"; /* -FNXC:GitLabTracking 2026-07-16-05:38: -GitLab tracking must round-trip through the shared TaskStore registry on every -live and soft-deleted read surface. A persisted empty object means not filed -for analytics and must remain distinct from absent tracking, which hydrates as undefined. +FNXC:GitLabTracking 2026-07-16-13:00: +GitLab import provenance must round-trip through every shared TaskStore read path +and archive/restore. Missing tracking remains undefined, while a populated item +must retain its full payload so imports can be reconciled after restoration. */ pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => { const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({ @@ -37,30 +37,26 @@ pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => { createdAt: "2026-07-16T00:00:00.000Z", }; - it("round-trips GitLab tracking across live and soft-deleted task reads", async () => { + it("round-trips GitLab tracking across every live read and archive restore", async () => { const store = h.store(); - const tracked = await store.createTask({ - description: "Tracked GitLab task", - sourceIssue: { - provider: "gitlab", - repository: "acme/app", - externalIssueId: "42", - issueNumber: 2, - url: item.url, - }, - gitlabTracking: { item }, - }); - const empty = await store.createTask({ description: "Empty GitLab tracking", gitlabTracking: {} }); - const absent = await store.createTask({ description: "Absent GitLab tracking" }); - - expect((await store.getTask(tracked.id))?.gitlabTracking?.item).toEqual(item); - expect((await store.getTask(tracked.id))?.sourceIssue).toEqual({ - provider: "gitlab", + const modifiedSince = "1970-01-01T00:00:00.000Z"; + const sourceIssue = { + provider: "gitlab" as const, repository: "acme/app", externalIssueId: "42", issueNumber: 2, url: item.url, + }; + const tracked = await store.createTask({ + description: "Tracked GitLab task", + sourceIssue, + gitlabTracking: { item }, }); + const absent = await store.createTask({ description: "Absent GitLab tracking" }); + + expect((await store.getTask(tracked.id))?.gitlabTracking?.item).toEqual(item); + expect((await store.getTask(tracked.id))?.sourceIssue).toEqual(sourceIssue); + expect((await store.getTask(absent.id))?.gitlabTracking).toBeUndefined(); for (const slim of [false, true]) { const listed = await store.listTasks({ slim }); @@ -69,17 +65,19 @@ pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => { if (slim) expect(listedTask?.log).toEqual([]); } - expect((await store.getTask(empty.id))?.gitlabTracking).toEqual({}); - expect((await store.listTasks({ slim: false })).find((task) => task.id === empty.id)?.gitlabTracking).toEqual({}); - expect((await store.getTask(absent.id))?.gitlabTracking).toBeUndefined(); + const searched = await store.searchTasks("Tracked GitLab task", { slim: true }); + expect(searched.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item); - await store.deleteTask(tracked.id); + const modified = await store.listTasksModifiedSince(modifiedSince); + expect(modified.tasks.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item); - for (const slim of [false, true]) { - const deleted = await store.listTasks({ includeDeleted: true, slim }); - expect(deleted.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item); - } - expect((await store.getTask(tracked.id, { includeDeleted: true }))?.gitlabTracking?.item).toEqual(item); - expect((await store.listTasksForGitlabTrackingReconcile()).tasks.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item); + await store.moveTask(tracked.id, "todo"); + await store.moveTask(tracked.id, "in-progress"); + await store.moveTask(tracked.id, "done"); + await store.archiveTask(tracked.id, false); + const restored = await store.unarchiveTask(tracked.id); + + expect(restored.gitlabTracking?.item).toEqual(item); + expect((await store.getTask(tracked.id))?.gitlabTracking?.item).toEqual(item); }); }); diff --git a/packages/core/src/task-store/archive-lifecycle-2.ts b/packages/core/src/task-store/archive-lifecycle-2.ts index f96fe66ea9..cb2c7d3284 100644 --- a/packages/core/src/task-store/archive-lifecycle-2.ts +++ b/packages/core/src/task-store/archive-lifecycle-2.ts @@ -49,6 +49,12 @@ export async function taskToArchiveEntryImpl(store: TaskStore, task: Task, archi prInfos: task.prInfos, issueInfo: task.issueInfo, githubTracking: task.githubTracking, + /* + FNXC:GitLabTracking 2026-07-16-13:00: + Archiving must retain GitLab provenance just as live TaskStore persistence does; + restored imports need their original GitLab tracking item for reconciliation. + */ + gitlabTracking: task.gitlabTracking, sourceIssue: task.sourceIssue, attachments: task.attachments, comments: task.comments, @@ -410,6 +416,7 @@ export async function restoreFromArchiveImpl(store: TaskStore, entry: import(".. review: entry.review, issueInfo: entry.issueInfo, githubTracking: entry.githubTracking, + gitlabTracking: entry.gitlabTracking, sourceIssue: entry.sourceIssue, attachments: entry.attachments, log: [...entry.log, { timestamp: new Date().toISOString(), action: "Task restored from archive" }], diff --git a/packages/core/src/task-store/serialization.ts b/packages/core/src/task-store/serialization.ts index ea7bc1aaaa..a25cdc3abd 100644 --- a/packages/core/src/task-store/serialization.ts +++ b/packages/core/src/task-store/serialization.ts @@ -333,6 +333,7 @@ export function archiveEntryToTask( prInfos: slim ? undefined : entry.prInfos, issueInfo: slim ? undefined : entry.issueInfo, githubTracking: entry.githubTracking, + gitlabTracking: entry.gitlabTracking, sourceIssue: slim ? undefined : entry.sourceIssue, attachments: slim ? undefined : entry.attachments, comments: entry.comments,