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";
|
} from "../../__test-utils__/pg-test-harness.js";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:GitLabTracking 2026-07-16-05:38:
|
FNXC:GitLabTracking 2026-07-16-13:00:
|
||||||
GitLab tracking must round-trip through the shared TaskStore registry on every
|
GitLab import provenance must round-trip through every shared TaskStore read path
|
||||||
live and soft-deleted read surface. A persisted empty object means not filed
|
and archive/restore. Missing tracking remains undefined, while a populated item
|
||||||
for analytics and must remain distinct from absent tracking, which hydrates as undefined.
|
must retain its full payload so imports can be reconciled after restoration.
|
||||||
*/
|
*/
|
||||||
pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => {
|
pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => {
|
||||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
@@ -37,30 +37,26 @@ pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => {
|
|||||||
createdAt: "2026-07-16T00:00:00.000Z",
|
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 store = h.store();
|
||||||
const tracked = await store.createTask({
|
const modifiedSince = "1970-01-01T00:00:00.000Z";
|
||||||
description: "Tracked GitLab task",
|
const sourceIssue = {
|
||||||
sourceIssue: {
|
provider: "gitlab" as const,
|
||||||
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",
|
|
||||||
repository: "acme/app",
|
repository: "acme/app",
|
||||||
externalIssueId: "42",
|
externalIssueId: "42",
|
||||||
issueNumber: 2,
|
issueNumber: 2,
|
||||||
url: item.url,
|
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]) {
|
for (const slim of [false, true]) {
|
||||||
const listed = await store.listTasks({ slim });
|
const listed = await store.listTasks({ slim });
|
||||||
@@ -69,17 +65,19 @@ pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => {
|
|||||||
if (slim) expect(listedTask?.log).toEqual([]);
|
if (slim) expect(listedTask?.log).toEqual([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect((await store.getTask(empty.id))?.gitlabTracking).toEqual({});
|
const searched = await store.searchTasks("Tracked GitLab task", { slim: true });
|
||||||
expect((await store.listTasks({ slim: false })).find((task) => task.id === empty.id)?.gitlabTracking).toEqual({});
|
expect(searched.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item);
|
||||||
expect((await store.getTask(absent.id))?.gitlabTracking).toBeUndefined();
|
|
||||||
|
|
||||||
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]) {
|
await store.moveTask(tracked.id, "todo");
|
||||||
const deleted = await store.listTasks({ includeDeleted: true, slim });
|
await store.moveTask(tracked.id, "in-progress");
|
||||||
expect(deleted.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item);
|
await store.moveTask(tracked.id, "done");
|
||||||
}
|
await store.archiveTask(tracked.id, false);
|
||||||
expect((await store.getTask(tracked.id, { includeDeleted: true }))?.gitlabTracking?.item).toEqual(item);
|
const restored = await store.unarchiveTask(tracked.id);
|
||||||
expect((await store.listTasksForGitlabTrackingReconcile()).tasks.find((task) => task.id === tracked.id)?.gitlabTracking?.item).toEqual(item);
|
|
||||||
|
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,
|
prInfos: task.prInfos,
|
||||||
issueInfo: task.issueInfo,
|
issueInfo: task.issueInfo,
|
||||||
githubTracking: task.githubTracking,
|
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,
|
sourceIssue: task.sourceIssue,
|
||||||
attachments: task.attachments,
|
attachments: task.attachments,
|
||||||
comments: task.comments,
|
comments: task.comments,
|
||||||
@@ -410,6 +416,7 @@ export async function restoreFromArchiveImpl(store: TaskStore, entry: import("..
|
|||||||
review: entry.review,
|
review: entry.review,
|
||||||
issueInfo: entry.issueInfo,
|
issueInfo: entry.issueInfo,
|
||||||
githubTracking: entry.githubTracking,
|
githubTracking: entry.githubTracking,
|
||||||
|
gitlabTracking: entry.gitlabTracking,
|
||||||
sourceIssue: entry.sourceIssue,
|
sourceIssue: entry.sourceIssue,
|
||||||
attachments: entry.attachments,
|
attachments: entry.attachments,
|
||||||
log: [...entry.log, { timestamp: new Date().toISOString(), action: "Task restored from archive" }],
|
log: [...entry.log, { timestamp: new Date().toISOString(), action: "Task restored from archive" }],
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ export function archiveEntryToTask(
|
|||||||
prInfos: slim ? undefined : entry.prInfos,
|
prInfos: slim ? undefined : entry.prInfos,
|
||||||
issueInfo: slim ? undefined : entry.issueInfo,
|
issueInfo: slim ? undefined : entry.issueInfo,
|
||||||
githubTracking: entry.githubTracking,
|
githubTracking: entry.githubTracking,
|
||||||
|
gitlabTracking: entry.gitlabTracking,
|
||||||
sourceIssue: slim ? undefined : entry.sourceIssue,
|
sourceIssue: slim ? undefined : entry.sourceIssue,
|
||||||
attachments: slim ? undefined : entry.attachments,
|
attachments: slim ? undefined : entry.attachments,
|
||||||
comments: entry.comments,
|
comments: entry.comments,
|
||||||
|
|||||||
Reference in New Issue
Block a user