FN-8094: persist GitLab tracking metadata
Restore shared TaskStore persistence and hydration for GitLab tracking metadata. - Register GitLab tracking JSONB for task creation, updates, and row hydration. - Reuse the shared mapper during GitLab reconciliation and cover live/deleted reads. - Migrate GitLab extension tests to the PostgreSQL harness and add a patch changeset. Files changed: .changeset/fn-8094-gitlab-tracking-mapping.md | 7 ++ .../__tests__/extension-gitlab-tracking.test.ts | 129 ++++++++++----------- packages/cli/src/__tests__/pg-extension-harness.ts | 4 - .../store-gitlab-tracking-hydration.pg.test.ts | 85 ++++++++++++++ .../store-gitlab-tracking-reconcile.test.ts | 11 +- packages/core/src/task-store/persistence.ts | 9 +- packages/core/src/task-store/remaining-ops-2.ts | 10 +- packages/core/src/task-store/serialization.ts | 1 + packages/core/src/task-store/task-creation.ts | 2 + 9 files changed, 174 insertions(+), 84 deletions(-) Fusion-Task-Id: FN-8094 Fusion-Task-Lineage: 5e876856-cf42-4628-a5ef-ab562c1bf501 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8094-gitlab-tracking-mapping.md
Normal file
7
.changeset/fn-8094-gitlab-tracking-mapping.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Preserve GitLab import tracking metadata in normal task reads.
|
||||||
|
category: fix
|
||||||
|
dev: GitLab tracking now uses the shared TaskStore persistence and hydration registry.
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, beforeEach, expect, it, vi } from "vitest";
|
||||||
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
import {
|
||||||
import { tmpdir } from "node:os";
|
createMockApi,
|
||||||
import { join } from "node:path";
|
createPgExtensionHarness,
|
||||||
import { TaskStore } from "@fusion/core";
|
pgDescribe,
|
||||||
|
} from "./pg-extension-harness.js";
|
||||||
|
|
||||||
const gitlabIssues = vi.hoisted(() => ({
|
const gitlabIssues = vi.hoisted(() => ({
|
||||||
project: [{ resourceKind: "project_issue", id: 1, iid: 2, projectId: 3, projectPath: "g/p", title: "Project issue", description: "Body", webUrl: "https://gitlab.example.com/g/p/-/issues/2", state: "opened", labels: [] }],
|
project: [{ resourceKind: "project_issue", id: 1, iid: 2, projectId: 3, projectPath: "g/p", title: "Project issue", description: "Body", webUrl: "https://gitlab.example.com/g/p/-/issues/2", state: "opened", labels: [] }],
|
||||||
@@ -77,86 +78,82 @@ async function loadExtension() {
|
|||||||
return mod.default;
|
return mod.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupTools() {
|
const h = createPgExtensionHarness("fn-8094-gitlab");
|
||||||
const repoRoot = await mkdtemp(join(tmpdir(), "fn-7428-extension-gitlab-"));
|
|
||||||
const cwd = join(repoRoot, ".worktrees", "feature");
|
|
||||||
await mkdir(join(repoRoot, ".fusion"), { recursive: true });
|
|
||||||
const taskStore = new TaskStore(repoRoot, undefined, { inMemoryDb: false });
|
|
||||||
await taskStore.init();
|
|
||||||
await taskStore.updateSettings({ gitlabAuthToken: "glpat_test", gitlabInstanceUrl: "https://gitlab.example.com" });
|
|
||||||
taskStore.close();
|
|
||||||
|
|
||||||
|
async function setupTools() {
|
||||||
|
await h.store().updateSettings({ gitlabAuthToken: "glpat_test", gitlabInstanceUrl: "https://gitlab.example.com" });
|
||||||
const extension = await loadExtension();
|
const extension = await loadExtension();
|
||||||
const tools = new Map<string, any>();
|
const api = createMockApi();
|
||||||
extension({ registerTool: (def: any) => tools.set(def.name, def), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
|
extension({
|
||||||
return { repoRoot, cwd, tools };
|
...api,
|
||||||
|
registerShortcut: vi.fn(),
|
||||||
|
registerFlag: vi.fn(),
|
||||||
|
} as any);
|
||||||
|
return { cwd: h.rootDir(), tools: api.tools };
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
FNXC:PostgresCutover 2026-07-16-05:40:
|
||||||
|
This GitLab extension suite runs on the shared PostgreSQL harness instead of the
|
||||||
|
removed inMemoryDb runtime. Its preserved tracking assertions require FN-8094
|
||||||
|
rowToTask hydration so imported GitLab provenance survives ordinary store reads.
|
||||||
|
|
||||||
FNXC:GitLabExtension 2026-07-02-00:00:
|
FNXC:GitLabExtension 2026-07-02-00:00:
|
||||||
GitLab extension tools are HTTP API tools backed by configured GitLab token settings. They must expose project/group/MR schemas, create local GitLab source/tracking metadata, and never depend on a `glab` binary or real network.
|
GitLab extension tools are HTTP API tools backed by configured GitLab token settings. They must expose project/group/MR schemas, create local GitLab source/tracking metadata, and never depend on a `glab` binary or real network.
|
||||||
*/
|
*/
|
||||||
describe("extension GitLab import tools", () => {
|
pgDescribe("extension GitLab import tools", () => {
|
||||||
beforeEach(() => vi.clearAllMocks());
|
beforeAll(h.beforeAll);
|
||||||
|
beforeEach(async () => {
|
||||||
afterEach(() => vi.restoreAllMocks());
|
await h.beforeEach();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
await h.afterEach();
|
||||||
|
});
|
||||||
|
afterAll(h.afterAll);
|
||||||
|
|
||||||
it("registers browse/import tools for project issues, group issues, and merge requests", async () => {
|
it("registers browse/import tools for project issues, group issues, and merge requests", async () => {
|
||||||
const { repoRoot, tools } = await setupTools();
|
const { tools } = await setupTools();
|
||||||
try {
|
for (const name of [
|
||||||
for (const name of [
|
"fn_task_browse_gitlab_project_issues",
|
||||||
"fn_task_browse_gitlab_project_issues",
|
"fn_task_import_gitlab_project_issues",
|
||||||
"fn_task_import_gitlab_project_issues",
|
"fn_task_browse_gitlab_group_issues",
|
||||||
"fn_task_browse_gitlab_group_issues",
|
"fn_task_import_gitlab_group_issues",
|
||||||
"fn_task_import_gitlab_group_issues",
|
"fn_task_browse_gitlab_merge_requests",
|
||||||
"fn_task_browse_gitlab_merge_requests",
|
"fn_task_import_gitlab_merge_requests",
|
||||||
"fn_task_import_gitlab_merge_requests",
|
]) {
|
||||||
]) {
|
expect(tools.get(name), name).toBeTruthy();
|
||||||
expect(tools.get(name), name).toBeTruthy();
|
expect(JSON.stringify(tools.get(name).parameters)).toContain(name.includes("group") ? "group" : "project");
|
||||||
expect(JSON.stringify(tools.get(name).parameters)).toContain(name.includes("group") ? "group" : "project");
|
expect(tools.get(name).description).toMatch(/GitLab/);
|
||||||
expect(tools.get(name).description).toMatch(/GitLab/);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await rm(repoRoot, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("imports GitLab project, group, and MR resources with source and tracking metadata", async () => {
|
it("imports GitLab project, group, and MR resources with source and tracking metadata", async () => {
|
||||||
const { repoRoot, cwd, tools } = await setupTools();
|
const { cwd, tools } = await setupTools();
|
||||||
try {
|
const project = await tools.get("fn_task_import_gitlab_project_issues").execute("p", { project: "g/p", limit: 1 }, undefined, undefined, { cwd });
|
||||||
const project = await tools.get("fn_task_import_gitlab_project_issues").execute("p", { project: "g/p", limit: 1 }, undefined, undefined, { cwd });
|
const group = await tools.get("fn_task_import_gitlab_group_issues").execute("g", { group: "g", limit: 1 }, undefined, undefined, { cwd });
|
||||||
const group = await tools.get("fn_task_import_gitlab_group_issues").execute("g", { group: "g", limit: 1 }, undefined, undefined, { cwd });
|
const mr = await tools.get("fn_task_import_gitlab_merge_requests").execute("m", { project: "g/p", limit: 1 }, undefined, undefined, { cwd });
|
||||||
const mr = await tools.get("fn_task_import_gitlab_merge_requests").execute("m", { project: "g/p", limit: 1 }, undefined, undefined, { cwd });
|
|
||||||
|
|
||||||
expect(project.content[0].text).toContain("Imported 1 GitLab project issue tasks");
|
expect(project.content[0].text).toContain("Imported 1 GitLab project issue tasks");
|
||||||
expect(group.content[0].text).toContain("Imported 1 GitLab group issue tasks");
|
expect(group.content[0].text).toContain("Imported 1 GitLab group issue tasks");
|
||||||
expect(mr.content[0].text).toContain("Imported 1 GitLab merge request tasks");
|
expect(mr.content[0].text).toContain("Imported 1 GitLab merge request tasks");
|
||||||
|
|
||||||
const taskStore = new TaskStore(repoRoot, undefined, { inMemoryDb: false });
|
const tasks = await h.store().listTasks({ slim: false });
|
||||||
await taskStore.init();
|
expect(tasks.map((task) => task.sourceIssue?.provider)).toEqual(["gitlab", "gitlab", "gitlab"]);
|
||||||
const tasks = await taskStore.listTasks({ slim: false });
|
expect(tasks.map((task) => task.gitlabTracking?.item?.kind)).toEqual(["project_issue", "group_issue", "merge_request"]);
|
||||||
expect(tasks.map((task) => task.sourceIssue?.provider)).toEqual(["gitlab", "gitlab", "gitlab"]);
|
expect(tasks.find((task) => task.gitlabTracking?.item?.kind === "group_issue")?.gitlabTracking?.item?.groupPath).toBe("g");
|
||||||
expect(tasks.map((task) => task.gitlabTracking?.item?.kind)).toEqual(["project_issue", "group_issue", "merge_request"]);
|
expect(tasks.find((task) => task.gitlabTracking?.item?.kind === "merge_request")?.title).toMatch(/^Review MR !9:/);
|
||||||
expect(tasks.find((task) => task.gitlabTracking?.item?.kind === "group_issue")?.gitlabTracking?.item?.groupPath).toBe("g");
|
|
||||||
expect(tasks.find((task) => task.gitlabTracking?.item?.kind === "merge_request")?.title).toMatch(/^Review MR !9:/);
|
|
||||||
taskStore.close();
|
|
||||||
} finally {
|
|
||||||
await rm(repoRoot, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns human-readable auth errors without leaking token-like input", async () => {
|
it("returns human-readable auth errors without leaking token-like input", async () => {
|
||||||
const { repoRoot, cwd, tools } = await setupTools();
|
const { cwd, tools } = await setupTools();
|
||||||
try {
|
await h.store().updateSettings({ gitlabAuthToken: null as any });
|
||||||
const taskStore = new TaskStore(repoRoot, undefined, { inMemoryDb: false });
|
|
||||||
await taskStore.init();
|
|
||||||
await taskStore.updateSettings({ gitlabAuthToken: null as any });
|
|
||||||
taskStore.close();
|
|
||||||
|
|
||||||
await expect(tools.get("fn_task_browse_gitlab_project_issues").execute("p", { project: "g/p" }, undefined, undefined, { cwd }))
|
await expect(tools.get("fn_task_browse_gitlab_project_issues").execute("p", { project: "g/p" }, undefined, undefined, { cwd }))
|
||||||
.rejects.toThrow("GitLab auth requires a configured access token");
|
.resolves.toMatchObject({
|
||||||
} finally {
|
isError: true,
|
||||||
await rm(repoRoot, { recursive: true, force: true });
|
details: { error: "GitLab auth requires a configured access token" },
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -101,10 +101,6 @@ export function registerExtension(api: MockApi): void {
|
|||||||
kbExtension(api as unknown as ExtensionAPI);
|
kbExtension(api as unknown as ExtensionAPI);
|
||||||
}
|
}
|
||||||
|
|
||||||
// `kbExtension` is imported lazily-free via the default export below; keep the
|
|
||||||
// import at module scope so `registerExtension` can call it.
|
|
||||||
import kbExtension from "../extension.js";
|
|
||||||
|
|
||||||
export interface PgExtensionHarness {
|
export interface PgExtensionHarness {
|
||||||
/** The project rootDir the PG-backed store is scoped to (also the tool-call cwd). */
|
/** The project rootDir the PG-backed store is scoped to (also the tool-call cwd). */
|
||||||
readonly rootDir: () => string;
|
readonly rootDir: () => string;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "vitest";
|
||||||
|
import type { TaskGitLabTrackedItem } from "../../types.js";
|
||||||
|
import {
|
||||||
|
createSharedPgTaskStoreTestHarness,
|
||||||
|
pgDescribe,
|
||||||
|
type SharedPgTaskStoreHarness,
|
||||||
|
} 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.
|
||||||
|
*/
|
||||||
|
pgDescribe("TaskStore GitLab tracking hydration (PostgreSQL)", () => {
|
||||||
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
|
prefix: "fusion_gitlab_tracking_hydration",
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(h.beforeAll);
|
||||||
|
beforeEach(h.beforeEach);
|
||||||
|
afterEach(h.afterEach);
|
||||||
|
afterAll(h.afterAll);
|
||||||
|
|
||||||
|
const item: TaskGitLabTrackedItem = {
|
||||||
|
kind: "project_issue",
|
||||||
|
id: 42,
|
||||||
|
projectId: 7,
|
||||||
|
iid: 2,
|
||||||
|
projectPath: "acme/app",
|
||||||
|
groupPath: "acme",
|
||||||
|
url: "https://gitlab.example.com/acme/app/-/issues/2",
|
||||||
|
instanceUrl: "https://gitlab.example.com",
|
||||||
|
host: "gitlab.example.com",
|
||||||
|
title: "Persisted GitLab issue",
|
||||||
|
state: "opened",
|
||||||
|
createdAt: "2026-07-16T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("round-trips GitLab tracking across live and soft-deleted task reads", 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",
|
||||||
|
repository: "acme/app",
|
||||||
|
externalIssueId: "42",
|
||||||
|
issueNumber: 2,
|
||||||
|
url: item.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const slim of [false, true]) {
|
||||||
|
const listed = await store.listTasks({ slim });
|
||||||
|
const listedTask = listed.find((task) => task.id === tracked.id);
|
||||||
|
expect(listedTask?.gitlabTracking?.item).toEqual(item);
|
||||||
|
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();
|
||||||
|
|
||||||
|
await store.deleteTask(tracked.id);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,11 +23,10 @@ const gitlabItem: TaskGitLabTrackedItem = {
|
|||||||
/*
|
/*
|
||||||
* FNXC:GitLabReconcile 2026-07-12-00:00:
|
* FNXC:GitLabReconcile 2026-07-12-00:00:
|
||||||
* listTasksForGitlabTrackingReconcile returns soft-deleted tasks with
|
* listTasksForGitlabTrackingReconcile returns soft-deleted tasks with
|
||||||
* gitlab_tracking JSONB. The store read/write pipeline does not yet serialize
|
* gitlab_tracking JSONB through the same shared row mapper as normal task reads.
|
||||||
* gitlabTracking through createTask/updateTask (the feature is partial on this
|
* The test seeds the column directly via adminDb to exercise reconciliation of
|
||||||
* branch), so we seed the column directly via adminDb and assert the reconcile
|
* externally persisted tracking metadata. Archived tasks are a separate async
|
||||||
* API returns the right tasks. Archived tasks are a separate async subsystem
|
* subsystem not surfaced through this API (same limitation as the GitHub reconcile).
|
||||||
* not surfaced through this API (same limitation as the GitHub reconcile).
|
|
||||||
*/
|
*/
|
||||||
pgTest("TaskStore.listTasksForGitlabTrackingReconcile", () => {
|
pgTest("TaskStore.listTasksForGitlabTrackingReconcile", () => {
|
||||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
@@ -46,7 +45,7 @@ pgTest("TaskStore.listTasksForGitlabTrackingReconcile", () => {
|
|||||||
it("returns soft-deleted tasks with GitLab tracking, excludes active and non-tracked", async () => {
|
it("returns soft-deleted tasks with GitLab tracking, excludes active and non-tracked", async () => {
|
||||||
const store = h.store();
|
const store = h.store();
|
||||||
const softDeleted = await store.createTask({ description: "soft deleted" });
|
const softDeleted = await store.createTask({ description: "soft deleted" });
|
||||||
// Seed gitlab_tracking directly since updateTask does not persist it yet.
|
// Seed externally persisted gitlab_tracking directly for reconcile coverage.
|
||||||
await h.adminDb().execute(
|
await h.adminDb().execute(
|
||||||
sql`UPDATE project.tasks SET gitlab_tracking = ${JSON.stringify({ item: gitlabItem })}::jsonb WHERE id = ${softDeleted.id}`,
|
sql`UPDATE project.tasks SET gitlab_tracking = ${JSON.stringify({ item: gitlabItem })}::jsonb WHERE id = ${softDeleted.id}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export interface TaskRow {
|
|||||||
prInfos: string | null;
|
prInfos: string | null;
|
||||||
issueInfo: string | null;
|
issueInfo: string | null;
|
||||||
githubTracking: string | null;
|
githubTracking: string | null;
|
||||||
|
gitlabTracking: string | null;
|
||||||
sourceIssueProvider: string | null;
|
sourceIssueProvider: string | null;
|
||||||
sourceIssueRepository: string | null;
|
sourceIssueRepository: string | null;
|
||||||
sourceIssueExternalIssueId: string | null;
|
sourceIssueExternalIssueId: string | null;
|
||||||
@@ -169,7 +170,7 @@ PostgreSQL task JSONB conversion must use one registry for both descriptor write
|
|||||||
export const TASK_JSONB_COLUMNS: ReadonlySet<string> = new Set([
|
export const TASK_JSONB_COLUMNS: ReadonlySet<string> = new Set([
|
||||||
"dependencies", "steps", "customFields", "log", "attachments", "steeringComments",
|
"dependencies", "steps", "customFields", "log", "attachments", "steeringComments",
|
||||||
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "prInfos",
|
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "prInfos",
|
||||||
"issueInfo", "githubTracking", "mergeDetails", "workspaceWorktrees", "enabledWorkflowSteps",
|
"issueInfo", "githubTracking", "gitlabTracking", "mergeDetails", "workspaceWorktrees", "enabledWorkflowSteps",
|
||||||
"modifiedFiles", "scopeAutoWiden", "sourceMetadata", "tokenUsagePerModel",
|
"modifiedFiles", "scopeAutoWiden", "sourceMetadata", "tokenUsagePerModel",
|
||||||
"tokenBudgetOverride", "columnDwellMs", "workflowTransitionNotification",
|
"tokenBudgetOverride", "columnDwellMs", "workflowTransitionNotification",
|
||||||
]);
|
]);
|
||||||
@@ -297,6 +298,12 @@ export const TASK_COLUMN_DESCRIPTORS: TaskColumnDescriptor[] = [
|
|||||||
defineTaskColumn("prInfos", (task) => toJson(task.prInfos || [])),
|
defineTaskColumn("prInfos", (task) => toJson(task.prInfos || [])),
|
||||||
defineTaskColumn("issueInfo", (task) => toJsonNullable(task.issueInfo)),
|
defineTaskColumn("issueInfo", (task) => toJsonNullable(task.issueInfo)),
|
||||||
defineTaskColumn("githubTracking", (task) => toJsonNullable(task.githubTracking)),
|
defineTaskColumn("githubTracking", (task) => toJsonNullable(task.githubTracking)),
|
||||||
|
/*
|
||||||
|
FNXC:GitLabTracking 2026-07-16-05:34:
|
||||||
|
GitLab import provenance must use the shared task persistence registry like GitHub tracking.
|
||||||
|
This closes the partial-feature gap so create and update writes retain metadata for live reads.
|
||||||
|
*/
|
||||||
|
defineTaskColumn("gitlabTracking", (task) => toJsonNullable(task.gitlabTracking)),
|
||||||
defineTaskColumn("sourceIssueProvider", (task) => task.sourceIssue?.provider ?? null),
|
defineTaskColumn("sourceIssueProvider", (task) => task.sourceIssue?.provider ?? null),
|
||||||
defineTaskColumn("sourceIssueRepository", (task) => task.sourceIssue?.repository ?? null),
|
defineTaskColumn("sourceIssueRepository", (task) => task.sourceIssue?.repository ?? null),
|
||||||
defineTaskColumn("sourceIssueExternalIssueId", (task) => task.sourceIssue?.externalIssueId ?? null),
|
defineTaskColumn("sourceIssueExternalIssueId", (task) => task.sourceIssue?.externalIssueId ?? null),
|
||||||
|
|||||||
@@ -453,14 +453,10 @@ export async function listTasksForGitlabTrackingReconcileImpl(store: TaskStore,
|
|||||||
.offset(deletedOffset);
|
.offset(deletedOffset);
|
||||||
const deletedTasks = deletedRowsRaw.map((row) => {
|
const deletedTasks = deletedRowsRaw.map((row) => {
|
||||||
const raw = row as unknown as Record<string, unknown>;
|
const raw = row as unknown as Record<string, unknown>;
|
||||||
|
// FNXC:GitLabTracking 2026-07-16-05:36: rowToTask now hydrates GitLab
|
||||||
|
// tracking through the shared persistence registry, so reconcile uses
|
||||||
|
// the same authoritative mapper as every other live-task read.
|
||||||
const task = store.rowToTask(store.pgRowToTaskRow(raw));
|
const task = store.rowToTask(store.pgRowToTaskRow(raw));
|
||||||
// FNXC:GitLabTracking 2026-07-12-00:00: the generic row mapper does not
|
|
||||||
// yet include gitlabTracking (the feature is partial on this branch).
|
|
||||||
// Manually attach it from the raw jsonb column so reconcile callers get
|
|
||||||
// the tracking item they need to reconcile.
|
|
||||||
if (raw.gitlabTracking != null) {
|
|
||||||
task.gitlabTracking = raw.gitlabTracking as Task["gitlabTracking"];
|
|
||||||
}
|
|
||||||
task.timedExecutionMs = store.computeTimedExecutionMs(task.log);
|
task.timedExecutionMs = store.computeTimedExecutionMs(task.log);
|
||||||
task.log = [];
|
task.log = [];
|
||||||
return task;
|
return task;
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export function rowToTask(row: TaskRow): Task {
|
|||||||
})(),
|
})(),
|
||||||
issueInfo: fromJson<import("../types.js").IssueInfo>(row.issueInfo),
|
issueInfo: fromJson<import("../types.js").IssueInfo>(row.issueInfo),
|
||||||
githubTracking: fromJson<import("../types.js").TaskGithubTracking>(row.githubTracking) ?? undefined,
|
githubTracking: fromJson<import("../types.js").TaskGithubTracking>(row.githubTracking) ?? undefined,
|
||||||
|
gitlabTracking: fromJson<import("../types.js").TaskGitLabTracking>(row.gitlabTracking) ?? undefined,
|
||||||
sourceIssue: (() => {
|
sourceIssue: (() => {
|
||||||
if (
|
if (
|
||||||
row.sourceIssueProvider === null
|
row.sourceIssueProvider === null
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ export async function _createTaskInternalBackendImpl(store: TaskStore, input: Ta
|
|||||||
tokenUsage: input.tokenUsage,
|
tokenUsage: input.tokenUsage,
|
||||||
sourceIssue: input.sourceIssue,
|
sourceIssue: input.sourceIssue,
|
||||||
githubTracking: input.githubTracking,
|
githubTracking: input.githubTracking,
|
||||||
|
gitlabTracking: input.gitlabTracking,
|
||||||
sourceType: input.source?.sourceType ?? "unknown",
|
sourceType: input.source?.sourceType ?? "unknown",
|
||||||
sourceAgentId: input.source?.sourceAgentId,
|
sourceAgentId: input.source?.sourceAgentId,
|
||||||
sourceRunId: input.source?.sourceRunId,
|
sourceRunId: input.source?.sourceRunId,
|
||||||
@@ -828,6 +829,7 @@ export async function _createTaskInternalImpl(store: TaskStore, input: TaskCreat
|
|||||||
tokenUsage: input.tokenUsage,
|
tokenUsage: input.tokenUsage,
|
||||||
sourceIssue: input.sourceIssue,
|
sourceIssue: input.sourceIssue,
|
||||||
githubTracking: input.githubTracking,
|
githubTracking: input.githubTracking,
|
||||||
|
gitlabTracking: input.gitlabTracking,
|
||||||
sourceType: input.source?.sourceType ?? "unknown",
|
sourceType: input.source?.sourceType ?? "unknown",
|
||||||
sourceAgentId: input.source?.sourceAgentId,
|
sourceAgentId: input.source?.sourceAgentId,
|
||||||
sourceRunId: input.source?.sourceRunId,
|
sourceRunId: input.source?.sourceRunId,
|
||||||
|
|||||||
Reference in New Issue
Block a user