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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8083-gitlab-tracking-hydration.md
Normal file
7
.changeset/fn-8083-gitlab-tracking-hydration.md
Normal file
@@ -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.
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user