Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
f4682cea0a docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: feat: implement export tasks (#1260)\n\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-10-06 14:11:11 +00:00
11 changed files with 224 additions and 431 deletions

View File

@@ -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.

View File

@@ -7,13 +7,10 @@
"extension": "0.25.4" "extension": "0.25.4"
}, },
"changesets": [ "changesets": [
"brave-lions-sing",
"chore-fix-docs", "chore-fix-docs",
"cursor-slash-commands", "cursor-slash-commands",
"curvy-weeks-flow", "curvy-weeks-flow",
"easy-spiders-wave", "easy-spiders-wave",
"fix-mcp-connection-errors",
"fix-mcp-default-tasks-path",
"flat-cities-say", "flat-cities-say",
"forty-tables-invite", "forty-tables-invite",
"gentle-cats-dance", "gentle-cats-dance",

View File

@@ -1,22 +1,5 @@
# task-master-ai # task-master-ai
## 0.28.0-rc.2
### Minor Changes
- [#1273](https://github.com/eyaltoledano/claude-task-master/pull/1273) [`b43b7ce`](https://github.com/eyaltoledano/claude-task-master/commit/b43b7ce201625eee956fb2f8cd332f238bb78c21) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Add Codex CLI provider with OAuth authentication
- Added codex-cli provider for GPT-5 and GPT-5-Codex models (272K input / 128K output)
- OAuth-first authentication via `codex login` - no API key required
- Optional OPENAI_CODEX_API_KEY support
- Codebase analysis capabilities automatically enabled
- Command-specific settings and approval/sandbox modes
### Patch Changes
- [#1277](https://github.com/eyaltoledano/claude-task-master/pull/1277) [`7b5a7c4`](https://github.com/eyaltoledano/claude-task-master/commit/7b5a7c4495a68b782f7407fc5d0e0d3ae81f42f5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.
- [#1276](https://github.com/eyaltoledano/claude-task-master/pull/1276) [`caee040`](https://github.com/eyaltoledano/claude-task-master/commit/caee040907f856d31a660171c9e6d966f23c632e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
## 0.28.0-rc.1 ## 0.28.0-rc.1
### Patch Changes ### Patch Changes

View File

@@ -200,6 +200,75 @@ sidebarTitle: "CLI Commands"
``` ```
</Accordion> </Accordion>
<Accordion title="Authentication">
```bash
# Log in to tryhamster.com
task-master auth login
# Check authentication status
task-master auth status
# Log out
task-master auth logout
# Refresh authentication token
task-master auth refresh
```
Authentication is required for task export functionality. The login command will open your browser to authenticate with tryhamster.com.
</Accordion>
<Accordion title="Context Management">
```bash
# Show current workspace context (organization and brief)
task-master context
# Select an organization interactively
task-master context org
# Select a brief within your organization
task-master context brief
# Set context using a brief URL or ID
task-master context <brief-url-or-id>
# Clear current context
task-master context clear
# Set context explicitly
task-master context set --org=<org-id> --brief=<brief-id>
```
Context management allows you to select which organization and brief to work with. This is required for task export operations.
</Accordion>
<Accordion title="Export Tasks">
```bash
# Export tasks to a Hamster brief using current context
task-master export
# Export tasks to a specific brief by ID
task-master export --brief=<brief-id>
# Export tasks to a specific organization and brief
task-master export --org=<org-id> --brief=<brief-id>
# Export tasks using a Hamster brief URL
task-master export <hamster-brief-url>
# Export tasks with filtering options
task-master export --status=pending --exclude-subtasks
# Export tasks from a specific tag
task-master export --tag=<tag-name>
# Skip confirmation prompt
task-master export --yes
```
The export command requires authentication (`tm auth login`) and exports local tasks to Hamster briefs. Tasks are exported with their titles, descriptions, implementation details, test strategies, and metadata preserved.
</Accordion>
<Accordion title="Initialize a Project"> <Accordion title="Initialize a Project">
```bash ```bash
# Initialize a new project with Task Master structure # Initialize a new project with Task Master structure

View File

@@ -3,4 +3,31 @@ title: "What's New"
sidebarTitle: "What's New" sidebarTitle: "What's New"
--- ---
## Latest Features
### Task Export to Hamster Briefs
Export your local Task Master tasks directly to Hamster briefs for seamless integration with your workflow management system.
**New Commands:**
- `task-master export` - Export tasks to Hamster briefs
- `task-master auth` - Manage authentication with tryhamster.com
- `task-master context` - Manage workspace context (organization/brief selection)
**Key Features:**
- Export tasks with full metadata preservation (titles, descriptions, implementation details, test strategies)
- Flexible filtering options (by status, tag, exclude subtasks)
- Support for both brief IDs and URLs
- Interactive authentication and context management
- Batch export with error handling and progress reporting
**Getting Started:**
1. Authenticate: `task-master auth login`
2. Select context: `task-master context org` and `task-master context brief`
3. Export tasks: `task-master export`
See the [CLI Commands](/capabilities/cli-root-commands) documentation for complete usage examples.
---
An easy way to see the latest releases An easy way to see the latest releases

75
output.txt Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.28.0-rc.2", "version": "0.28.0-rc.1",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.", "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",

View File

@@ -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) {

View File

@@ -0,0 +1,14 @@
{
"tasks": [
{
"id": 1,
"dependencies": [],
"subtasks": [
{
"id": 1,
"dependencies": []
}
]
}
]
}

View File

@@ -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: []
}
]
};

View File

@@ -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');
});
});
}); });