appendModifiedFiles.e2e.test.ts•27.4 kB
import {
McpTestClient,
TestEnvironment,
createObjectContent,
createObjectFile,
readObjectFile,
type ObjectData,
} from "../utils";
describe("E2E Workflow - appendModifiedFiles", () => {
let testEnv: TestEnvironment;
let client: McpTestClient;
beforeEach(async () => {
testEnv = new TestEnvironment();
testEnv.setup();
client = new McpTestClient(testEnv.projectRoot);
await client.connect();
await client.callTool("activate", {
mode: "local",
projectRoot: testEnv.projectRoot,
});
}, 30000);
afterEach(async () => {
await client?.disconnect();
testEnv?.cleanup();
});
describe("Appending Modified Files to Different Object Types", () => {
it("should append modified files to task", async () => {
const taskData: ObjectData = {
id: "T-files-test",
title: "Files Test Task",
status: "in-progress",
priority: "medium",
affectedFiles: {
"existing.ts": "Original implementation",
},
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-files-test",
createObjectContent(taskData),
);
const result = await client.callTool("append_modified_files", {
id: "T-files-test",
filesChanged: {
"src/components/Button.tsx": "Added new button component",
"src/utils/helpers.ts": "Created utility functions",
},
});
expect(result.content[0].text).toContain("Successfully appended");
expect(result.content[0].text).toContain("2 modified files");
expect(result.content[0].text).toContain("T-files-test");
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-files-test.md",
);
expect(file.yaml.affectedFiles).toEqual({
"existing.ts": "Original implementation",
"src/components/Button.tsx": "Added new button component",
"src/utils/helpers.ts": "Created utility functions",
});
});
it("should append modified files to project", async () => {
const result = await client.callTool("create_issue", {
type: "project",
title: "Files Test Project",
});
const projectId = result.content[0].text.match(/ID: (P-[a-z-]+)/)![1];
const filesResult = await client.callTool("append_modified_files", {
id: projectId,
filesChanged: {
"README.md": "Updated project documentation",
"package.json": "Added new dependencies",
},
});
expect(filesResult.content[0].text).toContain("Successfully appended");
expect(filesResult.content[0].text).toContain("2 modified files");
const file = await readObjectFile(
testEnv.projectRoot,
`p/${projectId}/${projectId}.md`,
);
expect(file.yaml.affectedFiles).toEqual({
"README.md": "Updated project documentation",
"package.json": "Added new dependencies",
});
});
it("should append modified files to epic", async () => {
// Create project first
const projectResult = await client.callTool("create_issue", {
type: "project",
title: "Parent Project",
});
const projectId =
projectResult.content[0].text.match(/ID: (P-[a-z-]+)/)![1];
// Create epic
const epicResult = await client.callTool("create_issue", {
type: "epic",
title: "Files Test Epic",
parent: projectId,
});
const epicId = epicResult.content[0].text.match(/ID: (E-[a-z-]+)/)![1];
const filesResult = await client.callTool("append_modified_files", {
id: epicId,
filesChanged: {
"src/features/auth/index.ts": "Authentication feature implementation",
},
});
expect(filesResult.content[0].text).toContain("Successfully appended");
expect(filesResult.content[0].text).toContain("1 modified file");
const file = await readObjectFile(
testEnv.projectRoot,
`p/${projectId}/e/${epicId}/${epicId}.md`,
);
expect(file.yaml.affectedFiles).toEqual({
"src/features/auth/index.ts": "Authentication feature implementation",
});
});
it("should append modified files to feature", async () => {
const result = await client.callTool("create_issue", {
type: "feature",
title: "Files Test Feature",
});
const featureId = result.content[0].text.match(/ID: (F-[a-z-]+)/)![1];
const filesResult = await client.callTool("append_modified_files", {
id: featureId,
filesChanged: {
"src/components/Modal.tsx": "Implemented modal component",
"src/hooks/useModal.ts": "Custom hook for modal state",
"tests/Modal.test.tsx": "Unit tests for modal component",
},
});
expect(filesResult.content[0].text).toContain("Successfully appended");
expect(filesResult.content[0].text).toContain("3 modified files");
const file = await readObjectFile(
testEnv.projectRoot,
`f/${featureId}/${featureId}.md`,
);
expect(file.yaml.affectedFiles).toEqual({
"src/components/Modal.tsx": "Implemented modal component",
"src/hooks/useModal.ts": "Custom hook for modal state",
"tests/Modal.test.tsx": "Unit tests for modal component",
});
});
});
describe("File Merging Behavior", () => {
it("should merge descriptions for existing files", async () => {
const taskData: ObjectData = {
id: "T-merge-test",
title: "Merge Test Task",
status: "open",
priority: "low",
affectedFiles: {
"src/utils/helpers.ts": "Initial helper functions",
"src/components/Button.tsx": "Basic button component",
},
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-merge-test",
createObjectContent(taskData),
);
await client.callTool("append_modified_files", {
id: "T-merge-test",
filesChanged: {
"src/utils/helpers.ts": "Added validation functions",
"src/components/Input.tsx": "New input component",
},
});
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-merge-test.md",
);
expect(file.yaml.affectedFiles).toEqual({
"src/utils/helpers.ts":
"Initial helper functions; Added validation functions",
"src/components/Button.tsx": "Basic button component",
"src/components/Input.tsx": "New input component",
});
});
it("should handle complex file paths and descriptions", async () => {
const taskData: ObjectData = {
id: "T-complex-paths",
title: "Complex Paths Task",
status: "in-progress",
priority: "high",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-complex-paths",
createObjectContent(taskData),
);
const complexFiles = {
"src/components/forms/auth/LoginForm.tsx":
"Implemented login form with validation & error handling",
"tests/e2e/auth/login.spec.ts": "E2E tests for login flow",
"docs/api/authentication.md": "API documentation for auth endpoints",
"config/webpack.config.js":
"Updated webpack config for new build process",
};
await client.callTool("append_modified_files", {
id: "T-complex-paths",
filesChanged: complexFiles,
});
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-complex-paths.md",
);
expect(file.yaml.affectedFiles).toEqual(complexFiles);
});
it("should preserve special characters in descriptions", async () => {
const taskData: ObjectData = {
id: "T-special-chars",
title: "Special Characters Task",
status: "open",
priority: "medium",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-special-chars",
createObjectContent(taskData),
);
const specialDescriptions = {
"src/components/Modal.tsx":
"Added modal with escape key handling & backdrop click",
"src/styles/globals.css":
"Updated CSS variables for theme switching; added dark mode support",
"src/utils/api.ts":
"API client with retry logic (3 attempts) & error handling",
};
await client.callTool("append_modified_files", {
id: "T-special-chars",
filesChanged: specialDescriptions,
});
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-special-chars.md",
);
expect(file.yaml.affectedFiles).toEqual(specialDescriptions);
});
});
describe("Edge Cases", () => {
it("should handle empty filesChanged object", async () => {
const taskData: ObjectData = {
id: "T-empty-files",
title: "Empty Files Task",
status: "open",
priority: "medium",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-empty-files",
createObjectContent(taskData),
);
const result = await client.callTool("append_modified_files", {
id: "T-empty-files",
filesChanged: {},
});
expect(result.content[0].text).toContain("Successfully appended");
expect(result.content[0].text).toContain("0 modified files");
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-empty-files.md",
);
expect(file.yaml.affectedFiles || {}).toEqual({});
});
it("should handle single file modification", async () => {
const taskData: ObjectData = {
id: "T-single-file",
title: "Single File Task",
status: "open",
priority: "low",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-single-file",
createObjectContent(taskData),
);
const result = await client.callTool("append_modified_files", {
id: "T-single-file",
filesChanged: {
"CHANGELOG.md": "Added release notes for v1.2.0",
},
});
expect(result.content[0].text).toContain("Successfully appended");
expect(result.content[0].text).toContain("1 modified file");
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-single-file.md",
);
expect(file.yaml.affectedFiles).toEqual({
"CHANGELOG.md": "Added release notes for v1.2.0",
});
});
it("should handle files with empty descriptions", async () => {
const taskData: ObjectData = {
id: "T-empty-descriptions",
title: "Empty Descriptions Task",
status: "open",
priority: "medium",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-empty-descriptions",
createObjectContent(taskData),
);
await client.callTool("append_modified_files", {
id: "T-empty-descriptions",
filesChanged: {
"empty.ts": "",
"normal.ts": "Normal description",
},
});
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-empty-descriptions.md",
);
expect(file.yaml.affectedFiles).toEqual({
"empty.ts": "",
"normal.ts": "Normal description",
});
});
});
describe("Error Handling", () => {
it("should fail to append files to non-existent object", async () => {
const result = await client.callTool("append_modified_files", {
id: "T-nonexistent",
filesChanged: {
"test.ts": "This should fail",
},
});
expect(result.content[0].text).toContain("not found");
});
it("should handle invalid object IDs", async () => {
const result = await client.callTool("append_modified_files", {
id: "invalid-id-format",
filesChanged: {
"test.ts": "Invalid ID test",
},
});
expect(result.content[0].text).toContain("not found");
});
});
describe("Multiple Operations", () => {
it("should handle multiple sequential append operations", async () => {
const taskData: ObjectData = {
id: "T-sequential",
title: "Sequential Operations Task",
status: "in-progress",
priority: "high",
affectedFiles: {
"initial.ts": "Initial file",
},
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-sequential",
createObjectContent(taskData),
);
// First append
await client.callTool("append_modified_files", {
id: "T-sequential",
filesChanged: {
"first.ts": "First modification",
"initial.ts": "Updated initial file",
},
});
// Second append
await client.callTool("append_modified_files", {
id: "T-sequential",
filesChanged: {
"second.ts": "Second modification",
"first.ts": "Updated first file",
},
});
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-sequential.md",
);
expect(file.yaml.affectedFiles).toEqual({
"initial.ts": "Initial file; Updated initial file",
"first.ts": "First modification; Updated first file",
"second.ts": "Second modification",
});
});
it("should maintain affected files across different operations", async () => {
const taskData: ObjectData = {
id: "T-mixed-operations",
title: "Mixed Operations Task",
status: "open",
priority: "medium",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-mixed-operations",
createObjectContent(taskData),
);
// Append files
await client.callTool("append_modified_files", {
id: "T-mixed-operations",
filesChanged: {
"file1.ts": "First implementation",
},
});
// Append log (should not affect files)
await client.callTool("append_issue_log", {
id: "T-mixed-operations",
contents: "Added initial implementation",
});
// Append more files
await client.callTool("append_modified_files", {
id: "T-mixed-operations",
filesChanged: {
"file2.ts": "Second implementation",
},
});
const file = await readObjectFile(
testEnv.projectRoot,
"t/open/T-mixed-operations.md",
);
expect(file.yaml.affectedFiles).toEqual({
"file1.ts": "First implementation",
"file2.ts": "Second implementation",
});
expect(file.yaml.log).toContain("Added initial implementation");
});
});
describe("Recursive Parent File Updates", () => {
it("should recursively update parent objects when appending files to a task", async () => {
// Create project
const projectResult = await client.callTool("create_issue", {
type: "project",
title: "Recursive Files Test Project",
});
const projectId =
projectResult.content[0].text.match(/ID: (P-[a-z-]+)/)![1];
// Create epic
const epicResult = await client.callTool("create_issue", {
type: "epic",
title: "Recursive Files Epic",
parent: projectId,
});
const epicId = epicResult.content[0].text.match(/ID: (E-[a-z-]+)/)![1];
// Create feature
const featureResult = await client.callTool("create_issue", {
type: "feature",
title: "Recursive Files Feature",
parent: epicId,
});
const featureId =
featureResult.content[0].text.match(/ID: (F-[a-z-]+)/)![1];
// Create task
const taskResult = await client.callTool("create_issue", {
type: "task",
title: "Recursive Files Task",
parent: featureId,
});
const taskId = taskResult.content[0].text.match(/ID: (T-[a-z-]+)/)![1];
// Append files to the task
const result = await client.callTool("append_modified_files", {
id: taskId,
filesChanged: {
"src/api/endpoints.ts": "Implemented REST API endpoints",
"src/models/User.ts": "User data model definition",
"tests/api.test.ts": "API integration tests",
},
});
expect(result.content[0].text).toContain("Successfully appended");
expect(result.content[0].text).toContain("3 modified files");
// Verify task has the affected files
const taskFile = await readObjectFile(
testEnv.projectRoot,
`p/${projectId}/e/${epicId}/f/${featureId}/t/open/${taskId}.md`,
);
expect(taskFile.yaml.affectedFiles).toEqual({
"src/api/endpoints.ts": "Implemented REST API endpoints",
"src/models/User.ts": "User data model definition",
"tests/api.test.ts": "API integration tests",
});
// Verify feature inherited the same affected files
const featureFile = await readObjectFile(
testEnv.projectRoot,
`p/${projectId}/e/${epicId}/f/${featureId}/${featureId}.md`,
);
expect(featureFile.yaml.affectedFiles).toEqual({
"src/api/endpoints.ts": "Implemented REST API endpoints",
"src/models/User.ts": "User data model definition",
"tests/api.test.ts": "API integration tests",
});
// Verify epic inherited the same affected files
const epicFile = await readObjectFile(
testEnv.projectRoot,
`p/${projectId}/e/${epicId}/${epicId}.md`,
);
expect(epicFile.yaml.affectedFiles).toEqual({
"src/api/endpoints.ts": "Implemented REST API endpoints",
"src/models/User.ts": "User data model definition",
"tests/api.test.ts": "API integration tests",
});
// Verify project inherited the same affected files
const projectFile = await readObjectFile(
testEnv.projectRoot,
`p/${projectId}/${projectId}.md`,
);
expect(projectFile.yaml.affectedFiles).toEqual({
"src/api/endpoints.ts": "Implemented REST API endpoints",
"src/models/User.ts": "User data model definition",
"tests/api.test.ts": "API integration tests",
});
});
it("should merge files with existing parent affected files", async () => {
// Create feature with existing affected files
const featureData: ObjectData = {
id: "F-merge-parent-files",
title: "Merge Parent Files Feature",
status: "open",
priority: "medium",
childrenIds: ["T-merge-parent-task"],
affectedFiles: {
"src/core/engine.ts": "Core engine implementation",
"README.md": "Project documentation",
},
};
await createObjectFile(
testEnv.projectRoot,
"feature",
"F-merge-parent-files",
createObjectContent(featureData),
);
// Create task
const taskData: ObjectData = {
id: "T-merge-parent-task",
title: "Merge Parent Task",
status: "open",
priority: "medium",
parent: "F-merge-parent-files",
};
await createObjectFile(
testEnv.projectRoot,
"task",
"T-merge-parent-task",
createObjectContent(taskData),
{ featureId: "F-merge-parent-files", status: "open" },
);
// Append files to task (including one that overlaps with feature)
await client.callTool("append_modified_files", {
id: "T-merge-parent-task",
filesChanged: {
"src/core/engine.ts": "Enhanced engine with new algorithms",
"src/utils/helpers.ts": "Utility helper functions",
"tests/engine.test.ts": "Engine unit tests",
},
});
// Verify task has its affected files
const taskFile = await readObjectFile(
testEnv.projectRoot,
"f/F-merge-parent-files/t/open/T-merge-parent-task.md",
);
expect(taskFile.yaml.affectedFiles).toEqual({
"src/core/engine.ts": "Enhanced engine with new algorithms",
"src/utils/helpers.ts": "Utility helper functions",
"tests/engine.test.ts": "Engine unit tests",
});
// Verify feature has merged files (original + task files, with overlap merged)
const featureFile = await readObjectFile(
testEnv.projectRoot,
"f/F-merge-parent-files/F-merge-parent-files.md",
);
expect(featureFile.yaml.affectedFiles).toEqual({
"src/core/engine.ts":
"Core engine implementation; Enhanced engine with new algorithms",
"README.md": "Project documentation",
"src/utils/helpers.ts": "Utility helper functions",
"tests/engine.test.ts": "Engine unit tests",
});
});
it("should handle feature with no parent (partial hierarchy)", async () => {
// Create standalone feature
const featureResult = await client.callTool("create_issue", {
type: "feature",
title: "Standalone Parent Test Feature",
});
const featureId =
featureResult.content[0].text.match(/ID: (F-[a-z-]+)/)![1];
// Create task under the feature
const taskResult = await client.callTool("create_issue", {
type: "task",
title: "Standalone Parent Task",
parent: featureId,
});
const taskId = taskResult.content[0].text.match(/ID: (T-[a-z-]+)/)![1];
// Append files to task
await client.callTool("append_modified_files", {
id: taskId,
filesChanged: {
"src/standalone.ts": "Standalone feature implementation",
"docs/standalone.md": "Standalone feature documentation",
},
});
// Verify task has the files
const taskFile = await readObjectFile(
testEnv.projectRoot,
`f/${featureId}/t/open/${taskId}.md`,
);
expect(taskFile.yaml.affectedFiles).toEqual({
"src/standalone.ts": "Standalone feature implementation",
"docs/standalone.md": "Standalone feature documentation",
});
// Verify feature also has the files
const featureFile = await readObjectFile(
testEnv.projectRoot,
`f/${featureId}/${featureId}.md`,
);
expect(featureFile.yaml.affectedFiles).toEqual({
"src/standalone.ts": "Standalone feature implementation",
"docs/standalone.md": "Standalone feature documentation",
});
});
it("should work when appending files to a feature (not just tasks)", async () => {
// Create epic
const epicResult = await client.callTool("create_issue", {
type: "epic",
title: "Feature Parent Test Epic",
});
const epicId = epicResult.content[0].text.match(/ID: (E-[a-z-]+)/)![1];
// Create feature under epic
const featureResult = await client.callTool("create_issue", {
type: "feature",
title: "Feature Parent Test Feature",
parent: epicId,
});
const featureId =
featureResult.content[0].text.match(/ID: (F-[a-z-]+)/)![1];
// Append files directly to the feature
await client.callTool("append_modified_files", {
id: featureId,
filesChanged: {
"src/feature/core.ts": "Feature core implementation",
"src/feature/utils.ts": "Feature utility functions",
},
});
// Verify feature has the files
const featureFile = await readObjectFile(
testEnv.projectRoot,
`e/${epicId}/f/${featureId}/${featureId}.md`,
);
expect(featureFile.yaml.affectedFiles).toEqual({
"src/feature/core.ts": "Feature core implementation",
"src/feature/utils.ts": "Feature utility functions",
});
// Verify epic also inherited the files
const epicFile = await readObjectFile(
testEnv.projectRoot,
`e/${epicId}/${epicId}.md`,
);
expect(epicFile.yaml.affectedFiles).toEqual({
"src/feature/core.ts": "Feature core implementation",
"src/feature/utils.ts": "Feature utility functions",
});
});
it("should handle object with no parent gracefully", async () => {
// Create standalone task (no parent hierarchy)
const taskResult = await client.callTool("create_issue", {
type: "task",
title: "Standalone No Parent Task",
});
const taskId = taskResult.content[0].text.match(/ID: (T-[a-z-]+)/)![1];
// Append files to standalone task
const result = await client.callTool("append_modified_files", {
id: taskId,
filesChanged: {
"src/isolated.ts": "Isolated functionality",
},
});
expect(result.content[0].text).toContain("Successfully appended");
expect(result.content[0].text).toContain("1 modified file");
// Verify task has the files (and no parent to propagate to)
const taskFile = await readObjectFile(
testEnv.projectRoot,
`t/open/${taskId}.md`,
);
expect(taskFile.yaml.affectedFiles).toEqual({
"src/isolated.ts": "Isolated functionality",
});
});
it("should handle multiple sequential append operations with parent propagation", async () => {
// Create feature and task
const featureResult = await client.callTool("create_issue", {
type: "feature",
title: "Sequential Parent Updates Feature",
});
const featureId =
featureResult.content[0].text.match(/ID: (F-[a-z-]+)/)![1];
const taskResult = await client.callTool("create_issue", {
type: "task",
title: "Sequential Parent Updates Task",
parent: featureId,
});
const taskId = taskResult.content[0].text.match(/ID: (T-[a-z-]+)/)![1];
// First append operation
await client.callTool("append_modified_files", {
id: taskId,
filesChanged: {
"src/module1.ts": "First module implementation",
"tests/module1.test.ts": "First module tests",
},
});
// Second append operation
await client.callTool("append_modified_files", {
id: taskId,
filesChanged: {
"src/module2.ts": "Second module implementation",
"src/module1.ts": "Enhanced first module",
},
});
// Verify task has merged results from both operations
const taskFile = await readObjectFile(
testEnv.projectRoot,
`f/${featureId}/t/open/${taskId}.md`,
);
expect(taskFile.yaml.affectedFiles).toEqual({
"src/module1.ts": "First module implementation; Enhanced first module",
"tests/module1.test.ts": "First module tests",
"src/module2.ts": "Second module implementation",
});
// Verify feature also has the merged results
const featureFile = await readObjectFile(
testEnv.projectRoot,
`f/${featureId}/${featureId}.md`,
);
expect(featureFile.yaml.affectedFiles).toEqual({
"src/module1.ts": "First module implementation; Enhanced first module",
"tests/module1.test.ts": "First module tests",
"src/module2.ts": "Second module implementation",
});
});
});
});