/**
* Data Lifecycle Integration Tests
*
* This file orchestrates the complete data lifecycle for GitLab integration testing.
* Tests are dependency-chained to build up infrastructure progressively:
*
* Chain: Create Group → Create Project → Initialize Repository → Create Work Items → Create MRs → Test All Features → Cleanup
*
* CRITICAL RULES:
* - Tests MUST run in order (Jest --runInBand)
* - Each test depends on previous test data
* - Individual tests cannot run standalone
* - Cleanup only happens at the very end
* - All other test files import this shared data
*/
import { GITLAB_TOKEN, GITLAB_API_URL, updateTestData, getTestData } from "../setup/testConfig";
import { GraphQLClient } from "../../src/graphql/client";
import { ConnectionManager } from "../../src/services/ConnectionManager";
import { getWorkItemTypes } from "../../src/utils/workItemTypes";
import { gql } from "graphql-tag";
import { IntegrationTestHelper } from "./helpers/registry-helper";
describe("🔄 Data Lifecycle - Complete Infrastructure Setup", () => {
const timestamp = Date.now();
const baseTestName = `lifecycle-test-${timestamp}`;
let _client: GraphQLClient;
let connectionManager: ConnectionManager;
let helper: IntegrationTestHelper;
beforeAll(async () => {
if (!GITLAB_TOKEN || !GITLAB_API_URL) {
throw new Error("GITLAB_TOKEN and GITLAB_API_URL are required for lifecycle tests");
}
console.log(`🚀 Starting data lifecycle chain with timestamp: ${timestamp}`);
// 🚨 CRITICAL: Initialize GraphQL schema introspection FIRST
console.log("🔍 Initializing GraphQL schema introspection...");
connectionManager = ConnectionManager.getInstance();
await connectionManager.initialize();
_client = connectionManager.getClient();
console.log("✅ GraphQL schema introspection completed");
// Initialize integration test helper
helper = new IntegrationTestHelper();
await helper.initialize();
console.log("✅ Integration test helper initialized");
});
// Note: Cleanup moved to globalTeardown.js to run after ALL tests complete
describe("👤 Step 0: User Infrastructure", () => {
it("should create test user (for assignment and collaboration testing)", async () => {
console.log("🔧 Getting current user...");
try {
// Get the current user instead of creating a new one
const userResponse = await fetch(`${GITLAB_API_URL}/api/v4/user`, {
method: "GET",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
});
if (userResponse.ok) {
const user = await userResponse.json();
updateTestData({ user });
console.log(`✅ Found current user: ${user.id} (${user.username})`);
} else {
console.log(
`⚠️ Could not get current user: ${userResponse.status} ${userResponse.statusText}`
);
updateTestData({ user: null });
}
} catch (userError) {
console.log("⚠️ User lookup failed:", userError);
updateTestData({ user: null });
}
});
});
describe("📁 Step 1: Group Infrastructure", () => {
it("should create test group (foundation for all tests)", async () => {
console.log("🔧 Creating test group...");
const createResponse = await fetch(`${GITLAB_API_URL}/api/v4/groups`, {
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: `Test Group ${baseTestName}`,
path: baseTestName,
description: `Integration test group created at ${new Date().toISOString()} - contains all test infrastructure`,
visibility: "private",
}),
});
expect(createResponse.ok).toBe(true);
const group = await createResponse.json();
expect(group).toHaveProperty("id");
expect(group).toHaveProperty("path", baseTestName);
// Update shared test data
updateTestData({ group });
console.log(`✅ Created test group: ${group.id} (${group.path})`);
});
});
describe("📦 Step 2: Project Infrastructure", () => {
it("should create test project (depends on group)", async () => {
const testData = getTestData();
expect(testData.group?.id).toBeDefined();
console.log("🔧 Creating test project in group...");
const createResponse = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: `Test Project ${baseTestName}`,
namespace_id: testData.group!.id,
description: `Integration test project - contains repository, MRs, work items for testing`,
visibility: "private",
initialize_with_readme: false,
owner_id: testData.user?.id || undefined, // Set test user as project owner if available
}),
});
expect(createResponse.ok).toBe(true);
const project = await createResponse.json();
expect(project).toHaveProperty("id");
expect(project).toHaveProperty("namespace");
expect(project.namespace.id).toBe(testData.group!.id);
// Update shared test data
updateTestData({ project });
console.log(`✅ Created test project: ${project.id} in group ${testData.group!.id}`);
});
});
describe("🌳 Step 3: Repository Infrastructure", () => {
it("should initialize repository with files (depends on project)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
console.log("🔧 Initializing repository...");
const repository: { branches: any[]; files: any[]; tags: any[] } = {
branches: [],
files: [],
tags: [],
};
// Create initial files
const initialFiles = [
{ path: "README.md", content: "IyBUZXN0IFByb2plY3Q=", message: "Initial README" },
{ path: "src/main.js", content: "Y29uc29sZS5sb2coImhlbGxvIik=", message: "Add main.js" },
{ path: "docs/guide.md", content: "IyBHdWlkZQ==", message: "Add documentation" },
{ path: ".gitignore", content: "bm9kZV9tb2R1bGVzLw==", message: "Add gitignore" },
// CI config with spec.inputs for pipeline inputs testing (GitLab 15.5+)
// rules: when: manual - prevents auto-triggering pipelines on every commit
{
path: ".gitlab-ci.yml",
content:
"c3BlYzoKICBpbnB1dHM6CiAgICBlbnZpcm9ubWVudDoKICAgICAgdHlwZTogc3RyaW5nCiAgICAgIGRlZmF1bHQ6IHRlc3QKICAgIGRlYnVnOgogICAgICB0eXBlOiBib29sZWFuCiAgICAgIGRlZmF1bHQ6IGZhbHNlCiAgICBjb3VudDoKICAgICAgdHlwZTogbnVtYmVyCiAgICAgIGRlZmF1bHQ6IDEKCnRlc3Qtam9iOgogIHNjcmlwdDoKICAgIC0gZWNobyAiRW52aXJvbm1lbnQ6ICRbWyBpbnB1dHMuZW52aXJvbm1lbnQgXV0iCiAgICAtIGVjaG8gIkRlYnVnOiAkW1sgaW5wdXRzLmRlYnVnIF1dIgogICAgLSBlY2hvICJDb3VudDogJFtbIGlucHV0cy5jb3VudCBdXSIKICBydWxlczoKICAgIC0gd2hlbjogbWFudWFsCg==",
message: "Add CI config with inputs spec",
},
];
for (const file of initialFiles) {
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/repository/files/${encodeURIComponent(file.path)}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
branch: "main",
content: file.content,
commit_message: file.message,
}),
}
);
if (response.ok) {
const fileData = await response.json();
repository.files.push(fileData);
}
}
expect(repository.files.length).toBeGreaterThan(0);
// Update shared test data
updateTestData({ repository });
console.log(`✅ Initialized repository with ${repository.files.length} files`);
});
it("should create feature branches (depends on repository)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
console.log("🔧 Creating feature branches...");
const branches = ["feature/user-auth", "feature/api-endpoints", "hotfix/security-patch"];
const createdBranches: any[] = [];
for (const branchName of branches) {
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/repository/branches`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
branch: branchName,
ref: "main",
}),
}
);
if (response.ok) {
const branch = await response.json();
createdBranches.push(branch);
}
}
// Update repository with branches
const currentRepo = testData.repository || { branches: [], files: [], tags: [] };
currentRepo.branches = createdBranches;
updateTestData({ repository: currentRepo });
expect(createdBranches.length).toBeGreaterThan(0);
console.log(`✅ Created ${createdBranches.length} feature branches`);
});
it("should create repository tags (depends on repository)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
console.log("🔧 Creating repository tags...");
const tags = [
{ name: `v1.0.0-${timestamp}`, ref: "main", message: "Release 1.0.0" },
{ name: `v1.1.0-${timestamp}`, ref: "main", message: "Release 1.1.0" },
];
const createdTags: any[] = [];
for (const tagData of tags) {
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/repository/tags`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_name: tagData.name,
ref: tagData.ref,
message: tagData.message,
}),
}
);
if (response.ok) {
const tag = await response.json();
createdTags.push(tag);
}
}
// Update repository with tags
const currentRepo = getTestData().repository || { branches: [], files: [], tags: [] };
currentRepo.tags = createdTags;
updateTestData({ repository: currentRepo });
expect(createdTags.length).toBeGreaterThan(0);
console.log(`✅ Created ${createdTags.length} repository tags`);
});
});
describe("🏷️ Step 4: Project Metadata", () => {
it("should create labels (depends on project)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
console.log("🔧 Creating project labels...");
const labels = [
{ name: "bug", color: "#ff0000", description: "Bug reports" },
{ name: "feature", color: "#00ff00", description: "New features" },
{ name: "enhancement", color: "#0000ff", description: "Improvements" },
];
const createdLabels: any[] = [];
for (const labelData of labels) {
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/labels`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(labelData),
}
);
if (response.ok) {
const label = await response.json();
createdLabels.push(label);
}
}
updateTestData({ labels: createdLabels });
expect(createdLabels.length).toBeGreaterThan(0);
console.log(`✅ Created ${createdLabels.length} project labels`);
});
it("should create milestones (depends on project)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
console.log("🔧 Creating project milestones...");
const milestones = [
{
title: `Sprint 1 - ${timestamp}`,
description: "First sprint milestone",
due_date: "2025-12-31",
start_date: "2025-09-15",
},
{
title: `Release 1.0 - ${timestamp}`,
description: "Major release milestone",
due_date: "2025-11-30",
},
];
const createdMilestones: any[] = [];
for (const milestoneData of milestones) {
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/milestones`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(milestoneData),
}
);
if (response.ok) {
const milestone = await response.json();
createdMilestones.push(milestone);
}
}
updateTestData({ milestones: createdMilestones });
expect(createdMilestones.length).toBeGreaterThan(0);
console.log(`✅ Created ${createdMilestones.length} project milestones`);
});
});
describe("📋 Step 5: Work Items Infrastructure", () => {
it("should create PROJECT-level work items (Issues, Tasks - depends on project + labels + milestones)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
console.log(
"🔧 Creating PROJECT-level work items (Issues, Tasks) using GraphQL with dynamic type discovery..."
);
// Get work item types directly using utility function (not exposed as tool)
console.log("🔍 Getting work item types for project namespace using internal utility...");
const projectWorkItemTypes = await getWorkItemTypes(testData.project!.path_with_namespace);
console.log(
"📋 Available project work item types:",
projectWorkItemTypes.map(t => `${t.name}(${t.id})`).join(", ")
);
const issueType = projectWorkItemTypes.find(t => t.name === "Issue");
const taskType = projectWorkItemTypes.find(t => t.name === "Task");
expect(issueType).toBeDefined();
expect(taskType).toBeDefined();
const createdProjectWorkItems: any[] = [];
// 🚨 CRITICAL: Create diverse Issues/Tasks to test with and without widgets
// Get created labels and milestones for widget testing
const labels = testData.labels || [];
const milestones = testData.milestones || [];
const user = testData.user;
const projectWorkItemsData: Array<{
title: string;
description: string;
workItemType: string;
assigneeIds?: string[];
labelIds?: string[];
milestoneId?: string;
}> = [
{
title: `Test Issue (No Widgets) ${timestamp}`,
description: "Test issue with no widgets - PROJECT LEVEL ONLY (using handler)",
workItemType: "ISSUE",
// No assignees, labels, or milestones - tests conditional widget inclusion
},
{
title: `Test Issue (With Widgets) ${timestamp}`,
description: "Test issue with widgets - PROJECT LEVEL ONLY (using handler)",
workItemType: "ISSUE",
assigneeIds: user ? [`gid://gitlab/User/${user.id}`] : undefined,
labelIds: labels.length > 0 ? [labels[0].id.toString()] : undefined,
milestoneId: milestones.length > 0 ? milestones[0].id.toString() : undefined,
},
{
title: `Test Task (No Widgets) ${timestamp}`,
description: "Test task with no widgets - PROJECT LEVEL ONLY (using handler)",
workItemType: "TASK",
// No assignees, labels, or milestones - tests conditional widget inclusion
},
{
title: `Test Task (With Widgets) ${timestamp}`,
description: "Test task with widgets - PROJECT LEVEL ONLY (using handler)",
workItemType: "TASK",
assigneeIds: user ? [`gid://gitlab/User/${user.id}`] : undefined,
labelIds: labels.length > 0 ? [labels[0].id.toString()] : undefined,
milestoneId: milestones.length > 0 ? milestones[0].id.toString() : undefined,
},
];
console.log("🚨 TESTING: About to start work item creation loop");
for (const workItemData of projectWorkItemsData) {
console.log(`🚨 TESTING: Processing work item: ${workItemData.title}`);
try {
console.log(` 🔧 Creating ${workItemData.workItemType} via create_work_item handler...`);
// Step 1: Create work item with basic parameters (CREATE doesn't support widgets)
const workItem = (await helper.createWorkItem({
namespace: testData.project!.path_with_namespace,
title: workItemData.title,
workItemType: workItemData.workItemType,
description: workItemData.description,
})) as any;
// Step 2: Add widgets iteratively if data exists for each widget type
console.log(
` 🔍 Widget check: workItem=${!!workItem}, title="${workItemData.title}", includesWithWidgets=${workItemData.title?.includes("With Widgets")}`
);
if (workItem && workItemData.title?.includes("With Widgets")) {
console.log(` 🔧 Adding widgets to ${workItemData.workItemType}...`);
// Try to add assignees widget if assignee data exists
if (workItemData.assigneeIds && workItemData.assigneeIds.length > 0) {
try {
console.log(` 🔧 Adding assignees widget...`);
const assigneeUpdate = (await helper.updateWorkItem({
id: workItem.id,
assigneeIds: workItemData.assigneeIds,
})) as any;
if (assigneeUpdate) {
console.log(` ✅ Added assignees: ${workItemData.assigneeIds.length}`);
console.log(
` 🔍 Assignee update response widgets:`,
assigneeUpdate.widgets?.find((w: any) => w.type === "ASSIGNEES")?.assignees
?.nodes?.length || 0
);
Object.assign(workItem, assigneeUpdate);
}
} catch (assigneeError) {
console.log(
` ⚠️ Could not add assignees to ${workItemData.workItemType}:`,
assigneeError
);
}
}
// Try to add labels widget if label data exists
if (workItemData.labelIds && workItemData.labelIds.length > 0) {
try {
console.log(` 🔧 Adding labels widget...`);
const labelUpdate = (await helper.updateWorkItem({
id: workItem.id,
labelIds: workItemData.labelIds,
})) as any;
if (labelUpdate) {
console.log(` ✅ Added labels: ${workItemData.labelIds.length}`);
console.log(
` 🔍 Label update response widgets:`,
labelUpdate.widgets?.find((w: any) => w.type === "LABELS")?.labels?.nodes
?.length || 0
);
Object.assign(workItem, labelUpdate);
}
} catch (labelError) {
console.log(
` ⚠️ Could not add labels to ${workItemData.workItemType}:`,
labelError
);
}
}
// Try to add milestone widget if milestone data exists
if (workItemData.milestoneId) {
try {
console.log(` 🔧 Adding milestone widget...`);
const milestoneUpdate = (await helper.updateWorkItem({
id: workItem.id,
milestoneId: workItemData.milestoneId,
})) as any;
if (milestoneUpdate) {
console.log(` ✅ Added milestone: 1`);
Object.assign(workItem, milestoneUpdate);
}
} catch (milestoneError) {
console.log(
` ⚠️ Could not add milestone to ${workItemData.workItemType}:`,
milestoneError
);
}
}
// Try to add time tracking if supported
try {
const timeTrackingUpdate = (await helper.updateWorkItem({
id: workItem.id,
timeEstimate: "2h",
})) as any;
if (timeTrackingUpdate) {
console.log(" ✅ Added time tracking estimate");
Object.assign(workItem, timeTrackingUpdate);
}
} catch (timeTrackingError) {
console.log(
` Could not add time tracking to ${workItemData.workItemType}:`,
timeTrackingError
);
}
}
console.log(
` 🔍 Widget testing - ${workItemData.workItemType}:`,
workItemData.assigneeIds
? `assignees: ${workItemData.assigneeIds.length}`
: "no assignees",
workItemData.labelIds ? `labels: ${workItemData.labelIds.length}` : "no labels",
workItemData.milestoneId ? "milestone: 1" : "no milestone"
);
if (workItem) {
createdProjectWorkItems.push(workItem);
console.log(
` ✅ Created PROJECT-level ${workItemData.workItemType}: ${workItem.iid} (Type: ${workItem.workItemType.name})`
);
} else {
console.log(` ⚠️ Handler returned null for ${workItemData.workItemType}`);
}
} catch (error) {
console.log(
` ⚠️ Could not create PROJECT work item via handler: ${workItemData.title}`,
error
);
}
}
updateTestData({ workItems: createdProjectWorkItems });
expect(createdProjectWorkItems.length).toBeGreaterThan(0);
console.log(
`✅ Created ${createdProjectWorkItems.length} PROJECT-level work items using GraphQL`
);
});
it("should create GROUP-level work items (Epics - depends on group)", async () => {
const testData = getTestData();
expect(testData.group?.id).toBeDefined();
console.log(
"🔧 Creating GROUP-level work items (Epics) using GraphQL with dynamic type discovery..."
);
// Get work item types directly using utility function (not exposed as tool)
console.log("🔍 Getting work item types for group namespace using internal utility...");
const groupWorkItemTypes = await getWorkItemTypes(testData.group!.path);
console.log(
"📋 Available group work item types:",
groupWorkItemTypes.map(t => `${t.name}(${t.id})`).join(", ")
);
const epicType = groupWorkItemTypes.find(t => t.name === "Epic");
expect(epicType).toBeDefined();
const createdGroupWorkItems: any[] = [];
// 🚨 CRITICAL: Create diverse Epics to test with and without widgets
// Get created labels and milestones for widget testing
const labels = testData.labels || [];
const milestones = testData.milestones || [];
const user = testData.user;
const groupWorkItemsData: Array<{
title: string;
description: string;
workItemType: string;
assigneeIds?: string[];
labelIds?: string[];
milestoneId?: string;
}> = [
{
title: `Test Epic (No Widgets) ${timestamp}`,
description: "Test epic with no widgets - GROUP LEVEL ONLY (using handler)",
workItemType: "EPIC",
// No assignees, labels, or milestones - tests conditional widget inclusion
},
{
title: `Test Epic (With Widgets) ${timestamp}`,
description: "Test epic with widgets - GROUP LEVEL ONLY (using handler)",
workItemType: "EPIC",
assigneeIds: user ? [`gid://gitlab/User/${user.id}`] : undefined,
labelIds: labels.length > 0 ? [labels[0].id.toString()] : undefined,
milestoneId: milestones.length > 0 ? milestones[0].id.toString() : undefined,
},
];
for (const workItemData of groupWorkItemsData) {
try {
console.log(` 🔧 Creating ${workItemData.workItemType} via create_work_item handler...`);
// Step 1: Create work item with basic parameters (CREATE doesn't support widgets)
const workItem = (await helper.createWorkItem({
namespace: testData.group!.path,
title: workItemData.title,
workItemType: workItemData.workItemType,
description: workItemData.description,
})) as any;
// Step 2: Add widgets iteratively if data exists for each widget type
console.log(
` 🔍 Widget check: workItem=${!!workItem}, title="${workItemData.title}", includesWithWidgets=${workItemData.title?.includes("With Widgets")}`
);
if (workItem && workItemData.title?.includes("With Widgets")) {
console.log(` 🔧 Adding widgets to ${workItemData.workItemType}...`);
// Try to add assignees widget if assignee data exists
if (workItemData.assigneeIds && workItemData.assigneeIds.length > 0) {
try {
console.log(` 🔧 Adding assignees widget...`);
const assigneeUpdate = (await helper.updateWorkItem({
id: workItem.id,
assigneeIds: workItemData.assigneeIds,
})) as any;
if (assigneeUpdate) {
console.log(` ✅ Added assignees: ${workItemData.assigneeIds.length}`);
console.log(
` 🔍 Assignee update response widgets:`,
assigneeUpdate.widgets?.find((w: any) => w.type === "ASSIGNEES")?.assignees
?.nodes?.length || 0
);
Object.assign(workItem, assigneeUpdate);
}
} catch (assigneeError) {
console.log(
` ⚠️ Could not add assignees to ${workItemData.workItemType}:`,
assigneeError
);
}
}
// Try to add labels widget if label data exists
if (workItemData.labelIds && workItemData.labelIds.length > 0) {
try {
console.log(` 🔧 Adding labels widget...`);
const labelUpdate = (await helper.updateWorkItem({
id: workItem.id,
labelIds: workItemData.labelIds,
})) as any;
if (labelUpdate) {
console.log(` ✅ Added labels: ${workItemData.labelIds.length}`);
console.log(
` 🔍 Label update response widgets:`,
labelUpdate.widgets?.find((w: any) => w.type === "LABELS")?.labels?.nodes
?.length || 0
);
Object.assign(workItem, labelUpdate);
}
} catch (labelError) {
console.log(
` ⚠️ Could not add labels to ${workItemData.workItemType}:`,
labelError
);
}
}
// Try to add milestone widget if milestone data exists
if (workItemData.milestoneId) {
try {
console.log(` 🔧 Adding milestone widget...`);
const milestoneUpdate = (await helper.updateWorkItem({
id: workItem.id,
milestoneId: workItemData.milestoneId,
})) as any;
if (milestoneUpdate) {
console.log(` ✅ Added milestone: 1`);
Object.assign(workItem, milestoneUpdate);
}
} catch (milestoneError) {
console.log(
` ⚠️ Could not add milestone to ${workItemData.workItemType}:`,
milestoneError
);
}
}
}
console.log(
` 🔍 Widget testing - ${workItemData.workItemType}:`,
workItemData.assigneeIds
? `assignees: ${workItemData.assigneeIds.length}`
: "no assignees",
workItemData.labelIds ? `labels: ${workItemData.labelIds.length}` : "no labels",
workItemData.milestoneId ? "milestone: 1" : "no milestone"
);
if (workItem) {
createdGroupWorkItems.push(workItem);
console.log(
` ✅ Created GROUP-level ${workItemData.workItemType}: ${workItem.iid} (Type: ${workItem.workItemType.name})`
);
} else {
console.log(` ⚠️ Handler returned null for ${workItemData.workItemType}`);
}
} catch (error) {
console.log(
` ⚠️ Could not create GROUP work item via handler: ${workItemData.title}`,
error
);
}
}
// Store group work items separately
updateTestData({ groupWorkItems: createdGroupWorkItems });
expect(createdGroupWorkItems.length).toBeGreaterThan(0);
console.log(`✅ Created ${createdGroupWorkItems.length} GROUP-level work items (Epics)`);
});
it("should verify work items actually have widgets (assignees, labels, milestones)", async () => {
const testData = getTestData();
console.log("🔍 Verifying work items actually contain widgets...");
// Check project-level work items
if (testData.workItems && testData.workItems.length > 0) {
for (const workItem of testData.workItems) {
console.log(`📋 Checking PROJECT work item: ${workItem.title}`);
// Use get_work_item handler to fetch full work item with widgets
const fullWorkItem = (await helper.getWorkItem({ id: workItem.id })) as any;
expect(fullWorkItem).toBeDefined();
expect(fullWorkItem.widgets).toBeDefined();
expect(Array.isArray(fullWorkItem.widgets)).toBe(true);
const widgets = fullWorkItem.widgets;
const assigneesWidget = widgets.find((w: any) => w.type === "ASSIGNEES");
const labelsWidget = widgets.find((w: any) => w.type === "LABELS");
const milestoneWidget = widgets.find((w: any) => w.type === "MILESTONE");
console.log(` 📊 Found widgets: ${widgets.map((w: any) => w.type).join(", ")}`);
if (workItem.title.includes("(With Widgets)")) {
console.log(` 🔍 Verifying "With Widgets" work item has actual widget data...`);
if (assigneesWidget) {
console.log(
` 👤 Assignees widget:`,
assigneesWidget.assignees?.nodes?.length || 0,
"assignees"
);
if (testData.user) {
expect(assigneesWidget.assignees?.nodes?.length).toBeGreaterThan(0);
}
}
if (labelsWidget) {
console.log(
` 🏷️ Labels widget:`,
labelsWidget.labels?.nodes?.length || 0,
"labels"
);
// Note: Labels widget might be empty if labels weren't assigned during work item creation
// This is expected behavior - we're testing widget structure, not necessarily content
expect(labelsWidget.labels?.nodes?.length || 0).toBeGreaterThanOrEqual(0);
}
if (milestoneWidget?.milestone) {
console.log(` 📅 Milestone widget:`, milestoneWidget.milestone.title);
expect(milestoneWidget.milestone).toBeDefined();
}
} else if (workItem.title.includes("(No Widgets)")) {
console.log(` 🔍 Verifying "No Widgets" work item has empty widget data...`);
if (assigneesWidget) {
expect(assigneesWidget.assignees?.nodes?.length || 0).toBe(0);
}
if (labelsWidget) {
expect(labelsWidget.labels?.nodes?.length || 0).toBe(0);
}
if (milestoneWidget) {
expect(milestoneWidget.milestone).toBeFalsy();
}
}
}
}
// Check group-level work items (Epics)
if (testData.groupWorkItems && testData.groupWorkItems.length > 0) {
for (const workItem of testData.groupWorkItems) {
console.log(`📋 Checking GROUP work item: ${workItem.title}`);
// Use get_work_item handler to fetch full work item with widgets
const fullWorkItem = (await helper.getWorkItem({ id: workItem.id })) as any;
expect(fullWorkItem).toBeDefined();
expect(fullWorkItem.widgets).toBeDefined();
expect(Array.isArray(fullWorkItem.widgets)).toBe(true);
const widgets = fullWorkItem.widgets;
const assigneesWidget = widgets.find((w: any) => w.type === "ASSIGNEES");
const labelsWidget = widgets.find((w: any) => w.type === "LABELS");
const milestoneWidget = widgets.find((w: any) => w.type === "MILESTONE");
console.log(` 📊 Found widgets: ${widgets.map((w: any) => w.type).join(", ")}`);
if (workItem.title.includes("(With Widgets)")) {
console.log(` 🔍 Verifying Epic "With Widgets" has actual widget data...`);
if (assigneesWidget) {
console.log(
` 👤 Assignees widget:`,
assigneesWidget.assignees?.nodes?.length || 0,
"assignees"
);
if (testData.user) {
expect(assigneesWidget.assignees?.nodes?.length).toBeGreaterThan(0);
}
}
if (labelsWidget) {
console.log(
` 🏷️ Labels widget:`,
labelsWidget.labels?.nodes?.length || 0,
"labels"
);
// Note: Labels widget might be empty if labels weren't assigned during work item creation
// This is expected behavior - we're testing widget structure, not necessarily content
expect(labelsWidget.labels?.nodes?.length || 0).toBeGreaterThanOrEqual(0);
}
if (milestoneWidget?.milestone) {
console.log(` 📅 Milestone widget:`, milestoneWidget.milestone.title);
expect(milestoneWidget.milestone).toBeDefined();
}
}
}
}
console.log("✅ Widget verification completed");
}, 30000);
});
describe("🏗️ Step 5.5: Subgroup and Parent Epic Infrastructure", () => {
it("should create subgroup (depends on main group)", async () => {
const testData = getTestData();
expect(testData.group?.id).toBeDefined();
console.log("🔧 Creating subgroup...");
const subgroupData = {
name: `Test Subgroup ${timestamp}`,
path: `test-subgroup-${timestamp}`.toLowerCase(),
description: "Test subgroup for epic hierarchy testing",
visibility: "private",
};
const response = await fetch(`${GITLAB_API_URL}/api/v4/groups`, {
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...subgroupData,
parent_id: testData.group!.id,
}),
});
expect(response.ok).toBe(true);
const subgroup = await response.json();
updateTestData({ subgroup });
expect(subgroup.id).toBeDefined();
expect(subgroup.parent_id).toBe(testData.group!.id);
console.log(`✅ Created subgroup: ${subgroup.name} (ID: ${subgroup.id})`);
});
it("should create epic in subgroup with parent epic and due date, then close it (depends on subgroup + parent epic)", async () => {
const testData = getTestData();
expect(testData.subgroup?.id).toBeDefined();
expect(testData.groupWorkItems?.length).toBeGreaterThan(0);
console.log("🔧 Creating epic in subgroup with parent epic and due date...");
// Find a parent epic from the main group
const parentEpic = testData.groupWorkItems!.find(
(item: any) => item.workItemType.name === "Epic" || item.workItemType === "Epic"
);
expect(parentEpic).toBeDefined();
console.log(`📋 Using parent epic: ${parentEpic.title} (ID: ${parentEpic.id})`);
// Step 1: Create epic in subgroup with due date
const epicWithParentData = {
title: `Child Epic ${timestamp}`,
description: "Test epic in subgroup with parent epic and due date",
workItemType: "EPIC",
};
console.log(`🔧 Creating epic in subgroup: ${testData.subgroup!.path}...`);
const childEpic = (await helper.createWorkItem({
namespace: testData.subgroup!.full_path,
title: epicWithParentData.title,
workItemType: epicWithParentData.workItemType,
description: epicWithParentData.description,
})) as any;
expect(childEpic).toBeDefined();
expect(childEpic.id).toBeDefined();
console.log(`✅ Created child epic: ${childEpic.title} (ID: ${childEpic.id})`);
// Step 2: Set parent epic relationship and due date
console.log("🔧 Setting parent epic and due date...");
// Step 3: Set due date using dates widget
const dueDate = "2025-12-15"; // Future due date
console.log(`🔧 Setting due date: ${dueDate}...`);
// Create a separate curl command to set due date and parent as GitLab's widget system is complex
const _curlUpdateCommand = `curl -X POST "${GITLAB_API_URL}/api/v4/graphql" \\
-H "Authorization: Bearer ${GITLAB_TOKEN}" \\
-H "Content-Type: application/json" \\
-d '{
"query": "mutation($input: WorkItemUpdateInput!) { workItemUpdate(input: $input) { workItem { id title state widgets { type ... on WorkItemWidgetHierarchy { parent { id title } } ... on WorkItemWidgetStartAndDueDate { dueDate } } } errors } }",
"variables": {
"input": {
"id": "${childEpic.id}",
"hierarchyWidget": {
"parentId": "${parentEpic.id}"
},
"startAndDueDateWidget": {
"dueDate": "${dueDate}"
}
}
}
}'`;
console.log("🔧 Setting parent and due date via GraphQL...");
// We'll use our direct GraphQL client to update the epic
const connectionManager = ConnectionManager.getInstance();
const client = connectionManager.getClient();
try {
const updateResponse = await client.request(
gql`
mutation ($input: WorkItemUpdateInput!) {
workItemUpdate(input: $input) {
workItem {
id
title
state
widgets {
type
... on WorkItemWidgetHierarchy {
parent {
id
title
}
}
... on WorkItemWidgetStartAndDueDate {
dueDate
}
}
}
errors
}
}
`,
{
input: {
id: childEpic.id,
hierarchyWidget: {
parentId: parentEpic.id,
},
startAndDueDateWidget: {
dueDate: dueDate,
},
},
}
);
const typedResponse = updateResponse as any;
if (typedResponse.workItemUpdate?.errors?.length > 0) {
console.log(
`⚠️ GraphQL errors setting parent/due date: ${typedResponse.workItemUpdate.errors.join(", ")}`
);
} else {
console.log("✅ Set parent epic and due date successfully");
}
} catch (error) {
console.log(`⚠️ Could not set parent/due date via GraphQL: ${error}`);
// Continue with the test - the epic was created successfully
}
// Step 4: Close the epic
console.log("🔧 Closing the epic...");
const closedEpic = (await helper.updateWorkItem({
id: childEpic.id,
state: "CLOSE",
})) as any;
expect(closedEpic).toBeDefined();
// Verify the epic is closed
const finalEpic = (await helper.getWorkItem({ id: childEpic.id })) as any;
expect(finalEpic.state).toBe("CLOSED");
// Store the child epic in test data
updateTestData({ childEpic: finalEpic });
console.log(`✅ Successfully created, configured, and closed child epic: ${finalEpic.title}`);
console.log(`📋 Final epic state: ${finalEpic.state}`);
});
});
describe("🔀 Step 6: Merge Requests Infrastructure", () => {
it("should create merge requests (depends on branches + work items)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
expect(testData.repository?.branches.length).toBeGreaterThan(0);
console.log("🔧 Creating merge requests...");
const mergeRequests: any[] = [];
// Create MRs from feature branches
for (let i = 0; i < Math.min(2, testData.repository!.branches.length); i++) {
const branch = testData.repository!.branches[i];
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/merge_requests`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source_branch: branch.name,
target_branch: "main",
title: `Merge ${branch.name} - ${timestamp}`,
description: `Test merge request from ${branch.name} branch`,
labels: testData.labels?.[0]?.name || undefined,
milestone_id: testData.milestones?.[0]?.id || undefined,
assignee_id: testData.user?.id || undefined,
reviewer_ids: testData.user ? [testData.user.id] : undefined,
}),
}
);
if (response.ok) {
const mr = await response.json();
mergeRequests.push(mr);
}
}
// Update test data
updateTestData({ mergeRequests });
expect(mergeRequests.length).toBeGreaterThan(0);
console.log(`✅ Created ${mergeRequests.length} merge requests`);
});
it("should test MR approval workflow (approve, get_approval_state, unapprove)", async () => {
const testData = getTestData();
expect(testData.mergeRequests?.length).toBeGreaterThan(0);
console.log("🔧 Testing MR approval workflow...");
const mr = testData.mergeRequests![0];
const projectId = testData.project!.id.toString();
// Step 1: Get initial approval state
console.log(" 🔍 Getting initial approval state...");
const initialState = (await helper.manageMergeRequest({
action: "get_approval_state",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
expect(initialState).toBeDefined();
console.log(
` ✅ Got approval state: ${initialState.rules?.length || 0} rules, overwritten: ${initialState.approval_rules_overwritten || false}`
);
// Step 2: Approve the MR
console.log(" 👍 Approving MR...");
try {
const approveResult = (await helper.manageMergeRequest({
action: "approve",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
expect(approveResult).toBeDefined();
console.log(" ✅ MR approved successfully");
// Step 3: Get approval state again to verify
console.log(" 🔍 Verifying approval state after approve...");
const afterApprove = (await helper.manageMergeRequest({
action: "get_approval_state",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
expect(afterApprove).toBeDefined();
console.log(" ✅ Got approval state after approve");
// Step 4: Unapprove the MR
console.log(" 👎 Unapproving MR...");
const unapproveResult = (await helper.manageMergeRequest({
action: "unapprove",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
expect(unapproveResult).toBeDefined();
console.log(" ✅ MR unapproved successfully");
} catch (error: any) {
// Some GitLab instances may not allow self-approval or have restrictions
if (
error.message?.includes("401") ||
error.message?.includes("403") ||
error.message?.includes("already approved")
) {
console.log(` ⚠️ Approval operation restricted: ${error.message}`);
} else {
throw error;
}
}
console.log("✅ MR approval workflow test completed");
});
it("should test MR discussion thread operations (create, resolve, unresolve)", async () => {
const testData = getTestData();
expect(testData.mergeRequests?.length).toBeGreaterThan(0);
console.log("🔧 Testing MR discussion thread operations...");
const mr = testData.mergeRequests![0];
const projectId = testData.project!.id.toString();
// Step 1: Create a discussion thread
console.log(" 💬 Creating discussion thread...");
const thread = (await helper.manageMrDiscussion({
action: "thread",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
body: `Test discussion thread created at ${new Date().toISOString()}`,
})) as any;
expect(thread).toBeDefined();
expect(thread.id).toBeDefined();
console.log(` ✅ Created thread: ${thread.id}`);
// Store thread for later tests
updateTestData({ discussionThread: thread });
// Step 2: Resolve the thread
console.log(" ✅ Resolving thread...");
const resolvedThread = (await helper.manageMrDiscussion({
action: "resolve",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
discussion_id: thread.id,
resolved: true,
})) as any;
expect(resolvedThread).toBeDefined();
console.log(` ✅ Thread resolved`);
// Step 3: Unresolve the thread
console.log(" ↩️ Unresolving thread...");
const unresolvedThread = (await helper.manageMrDiscussion({
action: "resolve",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
discussion_id: thread.id,
resolved: false,
})) as any;
expect(unresolvedThread).toBeDefined();
console.log(` ✅ Thread unresolved`);
// Step 4: List discussions to verify
console.log(" 📋 Listing discussions...");
const discussions = (await helper.browseMrDiscussions({
action: "list",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any[];
expect(discussions).toBeDefined();
expect(Array.isArray(discussions)).toBe(true);
console.log(` ✅ Found ${discussions.length} discussions`);
console.log("✅ MR discussion thread operations test completed");
});
it("should test diff note creation with position (PR #95 validation)", async () => {
const testData = getTestData();
expect(testData.mergeRequests?.length).toBeGreaterThan(0);
expect(testData.project?.id).toBeDefined();
console.log("🔧 Testing diff note with position (PR #95 - flattenPositionToFormFields)...");
const mr = testData.mergeRequests![0];
const projectId = testData.project!.id.toString();
// Step 1: Add a file to the feature branch to create a diff
console.log(" 📝 Adding file to feature branch to create diff...");
const featureBranch = mr.source_branch;
const testFilePath = `test-diff-note-${Date.now()}.js`;
const testFileContent = Buffer.from(
`// Test file for diff note\nconst message = "Hello from diff note test";\nconsole.log(message);\n`
).toString("base64");
const createFileResponse = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/repository/files/${encodeURIComponent(testFilePath)}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
branch: featureBranch,
content: testFileContent,
encoding: "base64",
commit_message: "Add test file for diff note testing",
}),
}
);
if (!createFileResponse.ok) {
const errorText = await createFileResponse.text();
console.log(
` ⚠️ Could not create test file: ${createFileResponse.status} - ${errorText}`
);
console.log(" ⚠️ Skipping diff note test - no diff available");
return;
}
console.log(` ✅ Created test file: ${testFilePath}`);
// Step 2: Get updated MR with diff_refs (retry if not ready yet)
console.log(" 🔍 Getting MR diff_refs...");
let mrDetails: any = null;
let retries = 0;
const maxRetries = 3;
const retryDelay = 3000; // 3 seconds
while (retries < maxRetries) {
mrDetails = (await helper.browseMergeRequests({
action: "get",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
if (mrDetails.diff_refs?.base_sha) {
console.log(` ✅ diff_refs ready on attempt ${retries + 1}`);
break;
}
retries++;
if (retries < maxRetries) {
console.log(
` ⏳ diff_refs not ready, retrying in ${retryDelay}ms (attempt ${retries}/${maxRetries})...`
);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
if (!mrDetails?.diff_refs?.base_sha) {
throw new Error(
`GitLab failed to generate diff_refs after ${maxRetries} retries (${maxRetries * retryDelay}ms total). This indicates a serious problem with GitLab's diff generation.`
);
}
const { base_sha, head_sha, start_sha } = mrDetails.diff_refs;
console.log(
` 📋 diff_refs: base=${base_sha?.substring(0, 8)}, head=${head_sha?.substring(0, 8)}, start=${start_sha?.substring(0, 8)}`
);
// Step 3: Get MR diffs to find the new file
console.log(" 🔍 Getting MR diffs...");
const mrDiffs = (await helper.browseMergeRequests({
action: "diffs",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
const testFileDiff = mrDiffs.changes?.find((change: any) => change.new_path === testFilePath);
if (!testFileDiff) {
console.log(` ⚠️ Test file not found in diffs - skipping position test`);
return;
}
console.log(` ✅ Found test file in diffs: ${testFileDiff.new_path}`);
// Step 4: Create diff note with position using the handler (tests flattenPositionToFormFields)
console.log(" 📝 Creating diff note with position...");
const position = {
base_sha,
head_sha,
start_sha,
position_type: "text" as const,
new_path: testFilePath,
old_path: testFilePath,
new_line: 2, // Comment on line 2: const message = ...
};
try {
const diffNote = (await helper.manageMrDiscussion({
action: "thread",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
body: `Test diff note created at ${new Date().toISOString()} - validates PR #95 position encoding fix`,
position,
})) as any;
expect(diffNote).toBeDefined();
expect(diffNote.id).toBeDefined();
console.log(` ✅ Created diff note thread: ${diffNote.id}`);
// Verify the note has position data
const firstNote = diffNote.notes?.[0];
if (firstNote?.position) {
expect(firstNote.position.new_path).toBe(testFilePath);
expect(firstNote.position.new_line).toBe(2);
console.log(
` ✅ Diff note has correct position: ${firstNote.position.new_path}:${firstNote.position.new_line}`
);
}
// Store for later verification
updateTestData({ diffNoteThread: diffNote });
console.log("✅ Diff note with position test PASSED - PR #95 fix verified!");
} catch (error: any) {
// If we get a specific error about position, the fix might not be working
if (error.message?.includes("position") || error.message?.includes("400")) {
console.error(` ❌ FAILED: Position encoding error - PR #95 fix may not be working!`);
console.error(` Error: ${error.message}`);
throw error;
}
// Other errors might be GitLab configuration issues
console.log(` ⚠️ Could not create diff note: ${error.message}`);
console.log(" ⚠️ This may be a GitLab configuration issue, not a code issue");
}
});
it("should test draft note with position (validates flattenPositionToFormFields for drafts)", async () => {
const testData = getTestData();
expect(testData.mergeRequests?.length).toBeGreaterThan(0);
console.log("🔧 Testing draft note with position...");
const mr = testData.mergeRequests![0];
const projectId = testData.project!.id.toString();
// Get MR diff_refs
const mrDetails = (await helper.browseMergeRequests({
action: "get",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
if (!mrDetails.diff_refs?.head_sha) {
console.log(" ⚠️ No diff_refs available - skipping draft note position test");
return;
}
const { base_sha, head_sha, start_sha } = mrDetails.diff_refs;
// Get diffs to find a file
const mrDiffs = (await helper.browseMergeRequests({
action: "diffs",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
})) as any;
if (!mrDiffs.changes?.length) {
console.log(" ⚠️ No diffs available - skipping draft note position test");
return;
}
const firstDiff = mrDiffs.changes[0];
console.log(` 📝 Creating draft note with position on ${firstDiff.new_path}...`);
const position = {
base_sha,
head_sha,
start_sha,
position_type: "text" as const,
new_path: firstDiff.new_path,
old_path: firstDiff.old_path || firstDiff.new_path,
new_line: 1,
};
try {
const draftNote = (await helper.executeTool("manage_draft_notes", {
action: "create",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
note: `Draft diff note test at ${new Date().toISOString()} - validates position encoding`,
position,
})) as any;
expect(draftNote).toBeDefined();
expect(draftNote.id).toBeDefined();
console.log(` ✅ Created draft note: ${draftNote.id}`);
// Verify position was correctly encoded
if (draftNote.position) {
expect(draftNote.position.new_path).toBe(firstDiff.new_path);
console.log(` ✅ Draft note has correct position: ${draftNote.position.new_path}`);
}
// Clean up: delete the draft note
await helper.executeTool("manage_draft_notes", {
action: "delete",
project_id: projectId,
merge_request_iid: mr.iid.toString(),
draft_note_id: draftNote.id.toString(),
});
console.log(` 🧹 Cleaned up draft note`);
console.log("✅ Draft note with position test PASSED!");
} catch (error: any) {
if (error.message?.includes("position") || error.message?.includes("400")) {
console.error(` ❌ FAILED: Position encoding error for draft notes!`);
throw error;
}
console.log(` ⚠️ Could not create draft note: ${error.message}`);
}
});
});
describe("📬 Step 6.5: Todos Infrastructure", () => {
let createdTodoId: number | null = null;
it("should create a todo explicitly for testing (depends on work items)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
expect(testData.workItems?.length).toBeGreaterThan(0);
console.log("🔧 Creating todo explicitly for testing...");
// Find an issue to create a todo for
const issueWorkItem = testData.workItems!.find(
(item: any) => item.workItemType?.name === "Issue" || item.workItemType === "Issue"
);
if (!issueWorkItem) {
console.log("⚠️ No issues available to create todo");
return;
}
// Create a todo using the GitLab API directly
// POST /projects/:id/issues/:issue_iid/todo
const response = await fetch(
`${GITLAB_API_URL}/api/v4/projects/${testData.project!.id}/issues/${issueWorkItem.iid}/todo`,
{
method: "POST",
headers: {
Authorization: `Bearer ${GITLAB_TOKEN}`,
"Content-Type": "application/json",
},
}
);
if (response.ok) {
const todo = await response.json();
createdTodoId = todo.id;
updateTestData({ todos: [todo] });
console.log(`✅ Created todo: ${todo.id} for issue #${issueWorkItem.iid}`);
} else if (response.status === 304) {
// Todo already exists - get it from list
console.log("📋 Todo already exists, fetching from list...");
const todos = (await helper.listTodos({
state: "pending",
per_page: 50,
})) as any[];
const existingTodo = todos.find(
(t: any) => t.target_type === "Issue" && t.target?.iid === issueWorkItem.iid
);
if (existingTodo) {
createdTodoId = existingTodo.id;
updateTestData({ todos: [existingTodo] });
console.log(`✅ Found existing todo: ${existingTodo.id}`);
} else if (todos.length > 0) {
createdTodoId = todos[0].id;
updateTestData({ todos: [todos[0]] });
console.log(`✅ Using first available todo: ${todos[0].id}`);
}
} else {
console.log(`⚠️ Could not create todo: ${response.status} ${response.statusText}`);
}
});
it("should list todos (depends on created todo)", async () => {
console.log("🔍 Testing list_todos...");
// List all pending todos
const todos = (await helper.listTodos({
state: "pending",
per_page: 50,
})) as any[];
console.log(`📋 Found ${todos.length} pending todos`);
if (todos.length > 0) {
const firstTodo = todos[0];
expect(firstTodo).toHaveProperty("id");
expect(firstTodo).toHaveProperty("target_type");
expect(firstTodo).toHaveProperty("action_name");
console.log(
` ✅ First todo: ${firstTodo.target_type} - ${firstTodo.action_name} (ID: ${firstTodo.id})`
);
// Update createdTodoId if not already set
if (!createdTodoId) {
createdTodoId = firstTodo.id;
updateTestData({ todos: [firstTodo] });
}
} else {
console.log(" ⚠️ No pending todos found - subsequent tests will be skipped");
}
});
it("should filter todos by type", async () => {
console.log("🔍 Testing todos filtering by type...");
// Filter for MergeRequest todos
const mrTodos = (await helper.listTodos({
type: "MergeRequest",
per_page: 20,
})) as any[];
console.log(`📋 Found ${mrTodos.length} MergeRequest todos`);
// Filter for Issue todos
const issueTodos = (await helper.listTodos({
type: "Issue",
per_page: 20,
})) as any[];
console.log(`📋 Found ${issueTodos.length} Issue todos`);
// Verify type filtering works
for (const todo of mrTodos) {
expect(todo.target_type).toBe("MergeRequest");
}
for (const todo of issueTodos) {
expect(todo.target_type).toBe("Issue");
}
console.log("✅ Todos type filtering works correctly");
});
it("should mark a todo as done", async () => {
if (!createdTodoId) {
console.log("⚠️ No todo available to test mark_done - skipping");
return;
}
console.log(`🔧 Testing mark todo as done with todo ID: ${createdTodoId}`);
// Step 1: Mark todo as done
console.log(" 🔧 Marking todo as done...");
const doneResult = (await helper.markTodoDone(createdTodoId)) as any;
expect(doneResult).toBeDefined();
expect(doneResult.state).toBe("done");
console.log(" ✅ Todo marked as done");
// Step 2: Verify it's in done state by listing done todos
const doneTodos = (await helper.listTodos({
state: "done",
per_page: 50,
})) as any[];
const foundDone = doneTodos.find((t: any) => t.id === createdTodoId);
expect(foundDone).toBeDefined();
console.log(" ✅ Verified todo is in done list");
// Note: restore (mark_as_pending) endpoint may not be available on all GitLab instances
// We test the core functionality (mark_done) which is the primary use case
console.log("✅ Todo mark_done test complete");
});
it("should mark all todos as done", async () => {
console.log("🔧 Testing mark all todos as done...");
// First check if there are any pending todos
const pendingBefore = (await helper.listTodos({
state: "pending",
per_page: 10,
})) as any[];
if (pendingBefore.length === 0) {
console.log(" ⚠️ No pending todos to mark as done - skipping");
return;
}
console.log(` 📋 Found ${pendingBefore.length} pending todos`);
// Mark all todos as done - handle potential API differences
try {
const result = (await helper.markAllTodosDone()) as any;
expect(result).toBeDefined();
expect(result.success).toBe(true);
console.log(" ✅ All todos marked as done via bulk API");
// Verify no pending todos remain
const pendingTodos = (await helper.listTodos({
state: "pending",
per_page: 10,
})) as any[];
expect(pendingTodos.length).toBe(0);
console.log(" ✅ Verified no pending todos remain");
} catch (_error) {
// Some GitLab instances may not support bulk mark_all_as_done
// Fall back to marking individually
console.log(" ⚠️ Bulk mark_all_done not available, marking individually...");
for (const todo of pendingBefore) {
try {
await helper.markTodoDone(todo.id);
} catch {
// Continue with others if one fails
}
}
console.log(" ✅ Marked todos as done individually");
}
console.log("✅ Mark all todos as done test complete");
});
});
describe("🔍 Step 7: Work Items Tools Validation", () => {
it("should test list_work_items with group namespace (Epics)", async () => {
const testData = getTestData();
expect(testData.group?.id).toBeDefined();
expect(testData.groupWorkItems?.length).toBeGreaterThan(0);
console.log("🔍 Testing list_work_items with group namespace...");
// Test group work items (Epics)
const groupResult = (await helper.listWorkItems({
namespace: testData.group!.path,
state: ["OPEN", "CLOSED"],
simple: true,
})) as any;
expect(groupResult).toBeDefined();
expect(groupResult.items).toBeDefined();
expect(Array.isArray(groupResult.items)).toBe(true);
expect(groupResult.items.length).toBeGreaterThan(0);
// Validate structure matches our simplified schema
expect(groupResult).toHaveProperty("hasMore");
expect(groupResult).toHaveProperty("endCursor");
console.log(` 📋 Found ${groupResult.items.length} group work items`);
// Verify we get Epics from group
const firstItem = groupResult.items[0];
expect(firstItem.workItemType).toBe("Epic");
console.log(` ✅ Confirmed group returns Epics: ${firstItem.title}`);
});
it("should test list_work_items with project namespace (Issues/Tasks)", async () => {
const testData = getTestData();
expect(testData.project?.id).toBeDefined();
expect(testData.workItems?.length).toBeGreaterThan(0);
console.log("🔍 Testing list_work_items with project namespace...");
// Test project work items (Issues/Tasks)
const projectResult = (await helper.listWorkItems({
namespace: testData.project!.path_with_namespace,
state: ["OPEN", "CLOSED"],
simple: true,
})) as any;
expect(projectResult).toBeDefined();
expect(projectResult.items).toBeDefined();
expect(Array.isArray(projectResult.items)).toBe(true);
expect(projectResult.items.length).toBeGreaterThan(0);
// Validate structure matches our simplified schema
expect(projectResult).toHaveProperty("hasMore");
expect(projectResult).toHaveProperty("endCursor");
console.log(` 📋 Found ${projectResult.items.length} project work items`);
// Verify we get Issues/Tasks from project
const firstItem = projectResult.items[0];
expect(["Issue", "Task"]).toContain(firstItem.workItemType);
console.log(
` ✅ Confirmed project returns Issues/Tasks: ${firstItem.title} (${firstItem.workItemType})`
);
});
it("should test list_work_items with type filtering", async () => {
const testData = getTestData();
console.log("🔍 Testing list_work_items with type filtering...");
// Test filtering for EPIC type in group
const epicResult = (await helper.listWorkItems({
namespace: testData.group!.path,
types: ["EPIC"],
simple: true,
})) as any;
expect(epicResult.items.length).toBeGreaterThan(0);
for (const item of epicResult.items) {
expect(item.workItemType).toBe("Epic");
}
console.log(` ✅ Epic filtering works: ${epicResult.items.length} epics found`);
// Test filtering for ISSUE type in project
const issueResult = (await helper.listWorkItems({
namespace: testData.project!.path_with_namespace,
types: ["ISSUE"],
simple: true,
})) as any;
expect(issueResult.items.length).toBeGreaterThan(0);
for (const item of issueResult.items) {
expect(item.workItemType).toBe("Issue");
}
console.log(` ✅ Issue filtering works: ${issueResult.items.length} issues found`);
});
it("should test list_work_items with state filtering", async () => {
const testData = getTestData();
console.log("🔍 Testing list_work_items with state filtering...");
// Test OPEN only
const openResult = (await helper.listWorkItems({
namespace: testData.project!.path_with_namespace,
state: ["OPEN"],
simple: true,
})) as any;
expect(openResult.items.length).toBeGreaterThan(0);
for (const item of openResult.items) {
expect(item.state).toBe("OPEN");
}
console.log(` ✅ OPEN state filtering: ${openResult.items.length} open items`);
// Test ALL states
const allResult = (await helper.listWorkItems({
namespace: testData.project!.path_with_namespace,
state: ["OPEN", "CLOSED"],
simple: true,
})) as any;
expect(allResult.items.length).toBeGreaterThanOrEqual(openResult.items.length);
console.log(` ✅ All states filtering: ${allResult.items.length} total items`);
});
it("should test list_work_items simple vs full structure", async () => {
const testData = getTestData();
console.log("🔍 Testing list_work_items simple vs full structure...");
// Test with simple=true (default)
const simpleResult = (await helper.listWorkItems({
namespace: testData.project!.path_with_namespace,
simple: true,
first: 1,
})) as any;
// Test with simple=false
const fullResult = (await helper.listWorkItems({
namespace: testData.project!.path_with_namespace,
simple: false,
first: 1,
})) as any;
expect(simpleResult.items.length).toBe(1);
expect(fullResult.items.length).toBe(1);
const simpleItem = simpleResult.items[0];
const fullItem = fullResult.items[0];
// Simple structure should have basic fields
expect(simpleItem).toHaveProperty("id");
expect(simpleItem).toHaveProperty("title");
expect(simpleItem).toHaveProperty("state");
// Full structure should have all fields including complex widgets
expect(fullItem).toHaveProperty("id");
expect(fullItem).toHaveProperty("title");
expect(fullItem).toHaveProperty("widgets");
console.log(` ✅ Simple structure works: ${Object.keys(simpleItem).length} fields`);
console.log(` ✅ Full structure works: ${Object.keys(fullItem).length} fields`);
});
});
describe("✅ Step 8: Infrastructure Validation", () => {
it("should validate complete test infrastructure is ready", async () => {
console.log("🔍 Validating complete test infrastructure...");
const testData = getTestData();
// Validate all components exist
expect(testData.group).toBeDefined();
expect(testData.project).toBeDefined();
expect(testData.repository).toBeDefined();
expect(testData.workItems?.length).toBeGreaterThan(0);
expect(testData.mergeRequests?.length).toBeGreaterThan(0);
expect(testData.labels?.length).toBeGreaterThan(0);
expect(testData.milestones?.length).toBeGreaterThan(0);
console.log("📊 Test Infrastructure Summary:");
console.log(
` 👤 User: ${testData.user?.id ? `${testData.user.id} (${testData.user.username})` : "N/A (no admin privileges)"}`
);
console.log(` 🏢 Group: ${testData.group?.id} (${testData.group?.path})`);
console.log(` 📦 Project: ${testData.project?.id}`);
console.log(` 📁 Files: ${testData.repository?.files?.length || 0}`);
console.log(` 🌿 Branches: ${testData.repository?.branches?.length || 0}`);
console.log(` 🏷️ Tags: ${testData.repository?.tags?.length || 0}`);
console.log(` 📋 Work Items: ${testData.workItems?.length || 0}`);
console.log(` 🔀 Merge Requests: ${testData.mergeRequests?.length || 0}`);
console.log(` 🏷️ Labels: ${testData.labels?.length || 0}`);
console.log(` 🎯 Milestones: ${testData.milestones?.length || 0}`);
console.log(` 📬 Todos: ${testData.todos?.length || 0} (cleared after testing)`);
console.log("✅ Complete test infrastructure ready for all schema validation tests");
});
});
});