Compare commits
1 Commits
ralph/chor
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a894969b5 |
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix cross-level task dependencies not being saved
|
|
||||||
|
|
||||||
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
|
|
||||||
@@ -50,7 +50,8 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"capabilities/mcp",
|
"capabilities/mcp",
|
||||||
"capabilities/cli-root-commands",
|
"capabilities/cli-root-commands",
|
||||||
"capabilities/task-structure"
|
"capabilities/task-structure",
|
||||||
|
"capabilities/rpg-method"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
34
output.txt
Normal file
34
output.txt
Normal file
File diff suppressed because one or more lines are too long
@@ -362,7 +362,7 @@ export class ExportService {
|
|||||||
|
|
||||||
if (useAPIEndpoint) {
|
if (useAPIEndpoint) {
|
||||||
// Use the new bulk import API endpoint
|
// Use the new bulk import API endpoint
|
||||||
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks`;
|
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
|
||||||
|
|
||||||
// Transform tasks to flat structure for API
|
// Transform tasks to flat structure for API
|
||||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
const flatTasks = this.transformTasksForBulkImport(tasks);
|
||||||
@@ -370,11 +370,11 @@ export class ExportService {
|
|||||||
// Prepare request body
|
// Prepare request body
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
source: 'task-master-cli',
|
source: 'task-master-cli',
|
||||||
|
accountId: orgId,
|
||||||
options: {
|
options: {
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
stopOnError: false
|
stopOnError: false
|
||||||
},
|
},
|
||||||
accountId: orgId,
|
|
||||||
tasks: flatTasks
|
tasks: flatTasks
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -312,23 +312,18 @@ async function removeDependency(tasksPath, taskId, dependencyId, context = {}) {
|
|||||||
|
|
||||||
// Check if the dependency exists by comparing string representations
|
// Check if the dependency exists by comparing string representations
|
||||||
const dependencyIndex = targetTask.dependencies.findIndex((dep) => {
|
const dependencyIndex = targetTask.dependencies.findIndex((dep) => {
|
||||||
// Direct string comparison (handles both numeric IDs and dot notation)
|
// Convert both to strings for comparison
|
||||||
const depStr = String(dep);
|
let depStr = String(dep);
|
||||||
if (depStr === normalizedDependencyId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For subtasks: handle numeric dependencies that might be references to other subtasks
|
// Special handling for numeric IDs that might be subtask references
|
||||||
// in the same parent (e.g., subtask 1.2 depending on subtask 1.1 stored as just "1")
|
|
||||||
if (typeof dep === 'number' && dep < 100 && isSubtask) {
|
if (typeof dep === 'number' && dep < 100 && isSubtask) {
|
||||||
|
// It's likely a reference to another subtask in the same parent task
|
||||||
|
// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
|
||||||
const [parentId] = formattedTaskId.split('.');
|
const [parentId] = formattedTaskId.split('.');
|
||||||
const fullSubtaskRef = `${parentId}.${dep}`;
|
depStr = `${parentId}.${dep}`;
|
||||||
if (fullSubtaskRef === normalizedDependencyId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return depStr === normalizedDependencyId;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dependencyIndex === -1) {
|
if (dependencyIndex === -1) {
|
||||||
@@ -401,9 +396,8 @@ function isCircularDependency(tasks, taskId, chain = []) {
|
|||||||
task = parentTask.subtasks.find((st) => st.id === subtaskId);
|
task = parentTask.subtasks.find((st) => st.id === subtaskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular task - handle both string and numeric task IDs
|
// Regular task
|
||||||
const taskIdNum = parseInt(taskIdStr, 10);
|
task = tasks.find((t) => String(t.id) === taskIdStr);
|
||||||
task = tasks.find((t) => t.id === taskIdNum || String(t.id) === taskIdStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
|||||||
14
tests/fixture/test-tasks.json
Normal file
14
tests/fixture/test-tasks.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"dependencies": [],
|
||||||
|
"subtasks": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"dependencies": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
tests/fixtures/sample-tasks.js
vendored
35
tests/fixtures/sample-tasks.js
vendored
@@ -88,38 +88,3 @@ export const emptySampleTasks = {
|
|||||||
},
|
},
|
||||||
tasks: []
|
tasks: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const crossLevelDependencyTasks = {
|
|
||||||
tasks: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Task 2 with subtasks',
|
|
||||||
description: 'Parent task',
|
|
||||||
status: 'pending',
|
|
||||||
dependencies: [],
|
|
||||||
subtasks: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Subtask 2.1',
|
|
||||||
description: 'First subtask',
|
|
||||||
status: 'pending',
|
|
||||||
dependencies: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Subtask 2.2',
|
|
||||||
description: 'Second subtask that should depend on Task 11',
|
|
||||||
status: 'pending',
|
|
||||||
dependencies: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
title: 'Task 11',
|
|
||||||
description: 'Top-level task that 2.2 should depend on',
|
|
||||||
status: 'done',
|
|
||||||
dependencies: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,47 +4,18 @@
|
|||||||
|
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
import {
|
import {
|
||||||
sampleTasks,
|
validateTaskDependencies,
|
||||||
crossLevelDependencyTasks
|
isCircularDependency,
|
||||||
} from '../fixtures/sample-tasks.js';
|
removeDuplicateDependencies,
|
||||||
|
cleanupSubtaskDependencies,
|
||||||
// Create mock functions that we can control in tests
|
ensureAtLeastOneIndependentSubtask,
|
||||||
const mockTaskExists = jest.fn();
|
validateAndFixDependencies,
|
||||||
const mockFormatTaskId = jest.fn();
|
canMoveWithDependencies
|
||||||
const mockFindCycles = jest.fn();
|
} from '../../scripts/modules/dependency-manager.js';
|
||||||
const mockLog = jest.fn();
|
import * as utils from '../../scripts/modules/utils.js';
|
||||||
const mockReadJSON = jest.fn();
|
import { sampleTasks } from '../fixtures/sample-tasks.js';
|
||||||
const mockWriteJSON = jest.fn();
|
|
||||||
|
|
||||||
// Mock the utils module using the same pattern as move-task-cross-tag.test.js
|
|
||||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
|
||||||
log: mockLog,
|
|
||||||
readJSON: mockReadJSON,
|
|
||||||
writeJSON: mockWriteJSON,
|
|
||||||
taskExists: mockTaskExists,
|
|
||||||
formatTaskId: mockFormatTaskId,
|
|
||||||
findCycles: mockFindCycles,
|
|
||||||
traverseDependencies: jest.fn(() => []),
|
|
||||||
isSilentMode: jest.fn(() => true),
|
|
||||||
findProjectRoot: jest.fn(() => '/test'),
|
|
||||||
resolveEnvVariable: jest.fn(() => undefined),
|
|
||||||
isEmpty: jest.fn((v) =>
|
|
||||||
v == null
|
|
||||||
? true
|
|
||||||
: Array.isArray(v)
|
|
||||||
? v.length === 0
|
|
||||||
: typeof v === 'object'
|
|
||||||
? Object.keys(v).length === 0
|
|
||||||
: false
|
|
||||||
),
|
|
||||||
// Common extras
|
|
||||||
enableSilentMode: jest.fn(),
|
|
||||||
disableSilentMode: jest.fn(),
|
|
||||||
getTaskManager: jest.fn(async () => ({})),
|
|
||||||
getTagAwareFilePath: jest.fn((basePath, _tag, projectRoot = '.') => basePath),
|
|
||||||
readComplexityReport: jest.fn(() => null)
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
jest.mock('path');
|
jest.mock('path');
|
||||||
jest.mock('chalk', () => ({
|
jest.mock('chalk', () => ({
|
||||||
green: jest.fn((text) => `<green>${text}</green>`),
|
green: jest.fn((text) => `<green>${text}</green>`),
|
||||||
@@ -56,16 +27,22 @@ jest.mock('chalk', () => ({
|
|||||||
|
|
||||||
jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
|
jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
|
||||||
|
|
||||||
// Now import SUT after mocks are in place
|
// Mock utils module
|
||||||
import {
|
const mockTaskExists = jest.fn();
|
||||||
validateTaskDependencies,
|
const mockFormatTaskId = jest.fn();
|
||||||
isCircularDependency,
|
const mockFindCycles = jest.fn();
|
||||||
removeDuplicateDependencies,
|
const mockLog = jest.fn();
|
||||||
cleanupSubtaskDependencies,
|
const mockReadJSON = jest.fn();
|
||||||
ensureAtLeastOneIndependentSubtask,
|
const mockWriteJSON = jest.fn();
|
||||||
validateAndFixDependencies,
|
|
||||||
canMoveWithDependencies
|
jest.mock('../../scripts/modules/utils.js', () => ({
|
||||||
} from '../../scripts/modules/dependency-manager.js';
|
log: mockLog,
|
||||||
|
readJSON: mockReadJSON,
|
||||||
|
writeJSON: mockWriteJSON,
|
||||||
|
taskExists: mockTaskExists,
|
||||||
|
formatTaskId: mockFormatTaskId,
|
||||||
|
findCycles: mockFindCycles
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('../../scripts/modules/ui.js', () => ({
|
jest.mock('../../scripts/modules/ui.js', () => ({
|
||||||
displayBanner: jest.fn()
|
displayBanner: jest.fn()
|
||||||
@@ -75,8 +52,8 @@ jest.mock('../../scripts/modules/task-manager.js', () => ({
|
|||||||
generateTaskFiles: jest.fn()
|
generateTaskFiles: jest.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use a temporary path for test files - Jest will clean up the temp directory
|
// Create a path for test files
|
||||||
const TEST_TASKS_PATH = '/tmp/jest-test-tasks.json';
|
const TEST_TASKS_PATH = 'tests/fixture/test-tasks.json';
|
||||||
|
|
||||||
describe('Dependency Manager Module', () => {
|
describe('Dependency Manager Module', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -707,8 +684,6 @@ describe('Dependency Manager Module', () => {
|
|||||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -762,8 +737,6 @@ describe('Dependency Manager Module', () => {
|
|||||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -777,8 +750,6 @@ describe('Dependency Manager Module', () => {
|
|||||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -832,8 +803,6 @@ describe('Dependency Manager Module', () => {
|
|||||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -947,297 +916,4 @@ describe('Dependency Manager Module', () => {
|
|||||||
expect(result.conflicts).toEqual([]);
|
expect(result.conflicts).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Cross-level dependency tests (Issue #542)', () => {
|
|
||||||
let originalExit;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
// Ensure a fresh module instance so ESM mocks apply to dynamic imports
|
|
||||||
jest.resetModules();
|
|
||||||
originalExit = process.exit;
|
|
||||||
process.exit = jest.fn();
|
|
||||||
|
|
||||||
// For ESM dynamic imports, use the same pattern
|
|
||||||
await jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
|
||||||
log: mockLog,
|
|
||||||
readJSON: mockReadJSON,
|
|
||||||
writeJSON: mockWriteJSON,
|
|
||||||
taskExists: mockTaskExists,
|
|
||||||
formatTaskId: mockFormatTaskId,
|
|
||||||
findCycles: mockFindCycles,
|
|
||||||
traverseDependencies: jest.fn(() => []),
|
|
||||||
isSilentMode: jest.fn(() => true),
|
|
||||||
findProjectRoot: jest.fn(() => '/test'),
|
|
||||||
resolveEnvVariable: jest.fn(() => undefined),
|
|
||||||
isEmpty: jest.fn((v) =>
|
|
||||||
v == null
|
|
||||||
? true
|
|
||||||
: Array.isArray(v)
|
|
||||||
? v.length === 0
|
|
||||||
: typeof v === 'object'
|
|
||||||
? Object.keys(v).length === 0
|
|
||||||
: false
|
|
||||||
),
|
|
||||||
enableSilentMode: jest.fn(),
|
|
||||||
disableSilentMode: jest.fn(),
|
|
||||||
getTaskManager: jest.fn(async () => ({})),
|
|
||||||
getTagAwareFilePath: jest.fn(
|
|
||||||
(basePath, _tag, projectRoot = '.') => basePath
|
|
||||||
),
|
|
||||||
readComplexityReport: jest.fn(() => null)
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Also mock transitive imports to keep dependency surface minimal
|
|
||||||
await jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
|
|
||||||
displayBanner: jest.fn()
|
|
||||||
}));
|
|
||||||
await jest.unstable_mockModule(
|
|
||||||
'../../scripts/modules/task-manager/generate-task-files.js',
|
|
||||||
() => ({ default: jest.fn() })
|
|
||||||
);
|
|
||||||
// Set up test data that matches the issue report
|
|
||||||
// Clone fixture data before each test to prevent mutation issues
|
|
||||||
mockReadJSON.mockImplementation(() =>
|
|
||||||
structuredClone(crossLevelDependencyTasks)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Configure mockTaskExists to properly validate cross-level dependencies
|
|
||||||
mockTaskExists.mockImplementation((tasks, taskId) => {
|
|
||||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
|
||||||
const [parentId, subtaskId] = taskId.split('.').map(Number);
|
|
||||||
const task = tasks.find((t) => t.id === parentId);
|
|
||||||
return (
|
|
||||||
task &&
|
|
||||||
task.subtasks &&
|
|
||||||
task.subtasks.some((st) => st.id === subtaskId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const numericId =
|
|
||||||
typeof taskId === 'string' ? parseInt(taskId, 10) : taskId;
|
|
||||||
return tasks.some((task) => task.id === numericId);
|
|
||||||
});
|
|
||||||
|
|
||||||
mockFormatTaskId.mockImplementation((id) => {
|
|
||||||
if (typeof id === 'string' && id.includes('.')) return id; // keep dot notation
|
|
||||||
return parseInt(id, 10); // normalize top-level task IDs to number
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.exit = originalExit;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow subtask to depend on top-level task', async () => {
|
|
||||||
const { addDependency } = await import(
|
|
||||||
'../../scripts/modules/dependency-manager.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test the specific scenario from Issue #542: subtask 2.2 depending on task 11
|
|
||||||
await addDependency(TEST_TASKS_PATH, '2.2', 11, { projectRoot: '/test' });
|
|
||||||
|
|
||||||
// Verify we wrote to the test path (and not the real tasks.json)
|
|
||||||
expect(mockWriteJSON).toHaveBeenCalledWith(
|
|
||||||
TEST_TASKS_PATH,
|
|
||||||
expect.anything(),
|
|
||||||
'/test',
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
|
||||||
'tasks/tasks.json',
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
// Get the specific write call for TEST_TASKS_PATH
|
|
||||||
const writeCall = mockWriteJSON.mock.calls.find(
|
|
||||||
([p]) => p === TEST_TASKS_PATH
|
|
||||||
);
|
|
||||||
expect(writeCall).toBeDefined();
|
|
||||||
const savedData = writeCall[1];
|
|
||||||
const parent2 = savedData.tasks.find((t) => t.id === 2);
|
|
||||||
const subtask22 = parent2.subtasks.find((st) => st.id === 2);
|
|
||||||
|
|
||||||
// Verify the dependency was actually added to subtask 2.2
|
|
||||||
expect(subtask22.dependencies).toContain(11);
|
|
||||||
// Also verify a success log was emitted
|
|
||||||
const successCall = mockLog.mock.calls.find(
|
|
||||||
([level]) => level === 'success'
|
|
||||||
);
|
|
||||||
expect(successCall).toBeDefined();
|
|
||||||
expect(successCall[1]).toContain('2.2');
|
|
||||||
expect(successCall[1]).toContain('11');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow top-level task to depend on subtask', async () => {
|
|
||||||
const { addDependency } = await import(
|
|
||||||
'../../scripts/modules/dependency-manager.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test reverse scenario: task 11 depending on subtask 2.1
|
|
||||||
await addDependency(TEST_TASKS_PATH, 11, '2.1', { projectRoot: '/test' });
|
|
||||||
|
|
||||||
// Stronger assertions for writeJSON call and locating the correct task
|
|
||||||
expect(mockWriteJSON).toHaveBeenCalledWith(
|
|
||||||
TEST_TASKS_PATH,
|
|
||||||
expect.anything(),
|
|
||||||
'/test',
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalledWith(
|
|
||||||
'tasks/tasks.json',
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
const writeCall = mockWriteJSON.mock.calls.find(
|
|
||||||
([p]) => p === TEST_TASKS_PATH
|
|
||||||
);
|
|
||||||
expect(writeCall).toBeDefined();
|
|
||||||
const savedData = writeCall[1];
|
|
||||||
const task11 = savedData.tasks.find((t) => t.id === 11);
|
|
||||||
|
|
||||||
// Verify the dependency was actually added to task 11
|
|
||||||
expect(task11.dependencies).toContain('2.1');
|
|
||||||
// Verify a success log was emitted mentioning both task 11 and subtask 2.1
|
|
||||||
const successCall = mockLog.mock.calls.find(
|
|
||||||
([level]) => level === 'success'
|
|
||||||
);
|
|
||||||
expect(successCall).toBeDefined();
|
|
||||||
expect(successCall[1]).toContain('11');
|
|
||||||
expect(successCall[1]).toContain('2.1');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should properly validate cross-level dependencies exist', async () => {
|
|
||||||
// Test that validation correctly identifies when a cross-level dependency target doesn't exist
|
|
||||||
mockTaskExists.mockImplementation((tasks, taskId) => {
|
|
||||||
// Simulate task 99 not existing
|
|
||||||
if (taskId === '99' || taskId === 99) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
|
||||||
const [parentId, subtaskId] = taskId.split('.').map(Number);
|
|
||||||
const task = tasks.find((t) => t.id === parentId);
|
|
||||||
return (
|
|
||||||
task &&
|
|
||||||
task.subtasks &&
|
|
||||||
task.subtasks.some((st) => st.id === subtaskId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const numericId =
|
|
||||||
typeof taskId === 'string' ? parseInt(taskId, 10) : taskId;
|
|
||||||
return tasks.some((task) => task.id === numericId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { addDependency } = await import(
|
|
||||||
'../../scripts/modules/dependency-manager.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
const exitError = new Error('process.exit invoked');
|
|
||||||
process.exit.mockImplementation(() => {
|
|
||||||
throw exitError;
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
addDependency(TEST_TASKS_PATH, '2.2', 99, { projectRoot: '/test' })
|
|
||||||
).rejects.toBe(exitError);
|
|
||||||
|
|
||||||
expect(process.exit).toHaveBeenCalledWith(1);
|
|
||||||
expect(mockWriteJSON).not.toHaveBeenCalled();
|
|
||||||
// Verify that an error was reported to the user
|
|
||||||
expect(mockLog).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should remove top-level task dependency from a subtask', async () => {
|
|
||||||
const { addDependency, removeDependency } = await import(
|
|
||||||
'../../scripts/modules/dependency-manager.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start with cloned data and add 11 to 2.2
|
|
||||||
await addDependency(TEST_TASKS_PATH, '2.2', 11, { projectRoot: '/test' });
|
|
||||||
|
|
||||||
// Get the saved data from the add operation
|
|
||||||
const addWriteCall = mockWriteJSON.mock.calls.find(
|
|
||||||
([p]) => p === TEST_TASKS_PATH
|
|
||||||
);
|
|
||||||
expect(addWriteCall).toBeDefined();
|
|
||||||
const dataWithDep = addWriteCall[1];
|
|
||||||
|
|
||||||
// Verify the dependency was added
|
|
||||||
const subtask22AfterAdd = dataWithDep.tasks
|
|
||||||
.find((t) => t.id === 2)
|
|
||||||
.subtasks.find((st) => st.id === 2);
|
|
||||||
expect(subtask22AfterAdd.dependencies).toContain(11);
|
|
||||||
|
|
||||||
// Clear mocks and re-setup mockReadJSON with the modified data
|
|
||||||
jest.clearAllMocks();
|
|
||||||
mockReadJSON.mockImplementation(() => structuredClone(dataWithDep));
|
|
||||||
|
|
||||||
await removeDependency(TEST_TASKS_PATH, '2.2', 11, {
|
|
||||||
projectRoot: '/test'
|
|
||||||
});
|
|
||||||
|
|
||||||
const writeCall = mockWriteJSON.mock.calls.find(
|
|
||||||
([p]) => p === TEST_TASKS_PATH
|
|
||||||
);
|
|
||||||
expect(writeCall).toBeDefined();
|
|
||||||
const saved = writeCall[1];
|
|
||||||
const subtask22 = saved.tasks
|
|
||||||
.find((t) => t.id === 2)
|
|
||||||
.subtasks.find((st) => st.id === 2);
|
|
||||||
expect(subtask22.dependencies).not.toContain(11);
|
|
||||||
// Verify success log was emitted
|
|
||||||
const successCall = mockLog.mock.calls.find(
|
|
||||||
([level]) => level === 'success'
|
|
||||||
);
|
|
||||||
expect(successCall).toBeDefined();
|
|
||||||
expect(successCall[1]).toContain('2.2');
|
|
||||||
expect(successCall[1]).toContain('11');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should remove subtask dependency from a top-level task', async () => {
|
|
||||||
const { addDependency, removeDependency } = await import(
|
|
||||||
'../../scripts/modules/dependency-manager.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add subtask dependency to task 11
|
|
||||||
await addDependency(TEST_TASKS_PATH, 11, '2.1', { projectRoot: '/test' });
|
|
||||||
|
|
||||||
// Get the saved data from the add operation
|
|
||||||
const addWriteCall = mockWriteJSON.mock.calls.find(
|
|
||||||
([p]) => p === TEST_TASKS_PATH
|
|
||||||
);
|
|
||||||
expect(addWriteCall).toBeDefined();
|
|
||||||
const dataWithDep = addWriteCall[1];
|
|
||||||
|
|
||||||
// Verify the dependency was added
|
|
||||||
const task11AfterAdd = dataWithDep.tasks.find((t) => t.id === 11);
|
|
||||||
expect(task11AfterAdd.dependencies).toContain('2.1');
|
|
||||||
|
|
||||||
// Clear mocks and re-setup mockReadJSON with the modified data
|
|
||||||
jest.clearAllMocks();
|
|
||||||
mockReadJSON.mockImplementation(() => structuredClone(dataWithDep));
|
|
||||||
|
|
||||||
await removeDependency(TEST_TASKS_PATH, 11, '2.1', {
|
|
||||||
projectRoot: '/test'
|
|
||||||
});
|
|
||||||
|
|
||||||
const writeCall = mockWriteJSON.mock.calls.find(
|
|
||||||
([p]) => p === TEST_TASKS_PATH
|
|
||||||
);
|
|
||||||
expect(writeCall).toBeDefined();
|
|
||||||
const saved = writeCall[1];
|
|
||||||
const task11 = saved.tasks.find((t) => t.id === 11);
|
|
||||||
expect(task11.dependencies).not.toContain('2.1');
|
|
||||||
// Verify success log was emitted
|
|
||||||
const successCall = mockLog.mock.calls.find(
|
|
||||||
([level]) => level === 'success'
|
|
||||||
);
|
|
||||||
expect(successCall).toBeDefined();
|
|
||||||
expect(successCall[1]).toContain('11');
|
|
||||||
expect(successCall[1]).toContain('2.1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user