feat: add --compact flag for minimal task list output (#1054)

* feat: add --compact flag for minimal task list output

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Ladi
2025-08-11 18:35:23 +02:00
committed by GitHub
parent 30ca144231
commit 782728ff95
7 changed files with 343 additions and 13 deletions

View File

@@ -22,7 +22,10 @@ jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
),
addComplexityToTask: jest.fn(),
readComplexityReport: jest.fn(() => null),
getTagAwareFilePath: jest.fn((tag, path) => '/mock/tagged/report.json')
getTagAwareFilePath: jest.fn((tag, path) => '/mock/tagged/report.json'),
stripAnsiCodes: jest.fn((text) =>
text ? text.replace(/\x1b\[[0-9;]*m/g, '') : text
)
}));
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
@@ -45,8 +48,13 @@ jest.unstable_mockModule(
);
// Import the mocked modules
const { readJSON, log, readComplexityReport, addComplexityToTask } =
await import('../../../../../scripts/modules/utils.js');
const {
readJSON,
log,
readComplexityReport,
addComplexityToTask,
stripAnsiCodes
} = await import('../../../../../scripts/modules/utils.js');
const { displayTaskList } = await import(
'../../../../../scripts/modules/ui.js'
);
@@ -584,4 +592,140 @@ describe('listTasks', () => {
expect(taskIds).toContain(5); // review task
});
});
describe('Compact output format', () => {
test('should output compact format when outputFormat is compact', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const tasksPath = 'tasks/tasks.json';
await listTasks(tasksPath, null, null, false, 'compact', {
tag: 'master'
});
expect(consoleSpy).toHaveBeenCalled();
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
// Strip ANSI color codes for testing
const cleanOutput = stripAnsiCodes(output);
// Should contain compact format elements: ID status title (priority) [→ dependencies]
expect(cleanOutput).toContain('1 done Setup Project (high)');
expect(cleanOutput).toContain(
'2 pending Implement Core Features (high) → 1'
);
consoleSpy.mockRestore();
});
test('should format single task compactly', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const tasksPath = 'tasks/tasks.json';
await listTasks(tasksPath, null, null, false, 'compact', {
tag: 'master'
});
expect(consoleSpy).toHaveBeenCalled();
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
// Should be compact (no verbose headers)
expect(output).not.toContain('Project Dashboard');
expect(output).not.toContain('Progress:');
consoleSpy.mockRestore();
});
test('should handle compact format with subtasks', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const tasksPath = 'tasks/tasks.json';
await listTasks(
tasksPath,
null,
null,
true, // withSubtasks = true
'compact',
{ tag: 'master' }
);
expect(consoleSpy).toHaveBeenCalled();
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
// Strip ANSI color codes for testing
const cleanOutput = stripAnsiCodes(output);
// Should handle both tasks and subtasks
expect(cleanOutput).toContain('1 done Setup Project (high)');
expect(cleanOutput).toContain('3.1 done Create Header Component');
consoleSpy.mockRestore();
});
test('should handle empty task list in compact format', async () => {
readJSON.mockReturnValue({ tasks: [] });
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const tasksPath = 'tasks/tasks.json';
await listTasks(tasksPath, null, null, false, 'compact', {
tag: 'master'
});
expect(consoleSpy).toHaveBeenCalledWith('No tasks found');
consoleSpy.mockRestore();
});
test('should format dependencies correctly with shared helper', async () => {
// Create mock tasks with various dependency scenarios
const tasksWithDeps = {
tasks: [
{
id: 1,
title: 'Task with no dependencies',
status: 'pending',
priority: 'medium',
dependencies: []
},
{
id: 2,
title: 'Task with few dependencies',
status: 'pending',
priority: 'high',
dependencies: [1, 3]
},
{
id: 3,
title: 'Task with many dependencies',
status: 'pending',
priority: 'low',
dependencies: [1, 2, 4, 5, 6, 7, 8, 9]
}
]
};
readJSON.mockReturnValue(tasksWithDeps);
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const tasksPath = 'tasks/tasks.json';
await listTasks(tasksPath, null, null, false, 'compact', {
tag: 'master'
});
expect(consoleSpy).toHaveBeenCalled();
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
// Strip ANSI color codes for testing
const cleanOutput = stripAnsiCodes(output);
// Should format tasks correctly with compact output including priority
expect(cleanOutput).toContain(
'1 pending Task with no dependencies (medium)'
);
expect(cleanOutput).toContain('Task with few dependencies');
expect(cleanOutput).toContain('Task with many dependencies');
// Should show dependencies with arrow when they exist
expect(cleanOutput).toMatch(/2.*→.*1,3/);
// Should truncate many dependencies with "+X more" format
expect(cleanOutput).toMatch(/3.*→.*1,2,4,5,6.*\(\+\d+ more\)/);
consoleSpy.mockRestore();
});
});
});

View File

@@ -0,0 +1,56 @@
/**
* Tests for the stripAnsiCodes utility function
*/
import { jest } from '@jest/globals';
// Import the module under test
const { stripAnsiCodes } = await import('../../scripts/modules/utils.js');
describe('stripAnsiCodes', () => {
test('should remove ANSI color codes from text', () => {
const textWithColors = '\x1b[31mRed text\x1b[0m \x1b[32mGreen text\x1b[0m';
const result = stripAnsiCodes(textWithColors);
expect(result).toBe('Red text Green text');
});
test('should handle text without ANSI codes', () => {
const plainText = 'This is plain text';
const result = stripAnsiCodes(plainText);
expect(result).toBe('This is plain text');
});
test('should handle empty string', () => {
const result = stripAnsiCodes('');
expect(result).toBe('');
});
test('should handle complex ANSI sequences', () => {
// Test with various ANSI escape sequences
const complexText =
'\x1b[1;31mBold red\x1b[0m \x1b[4;32mUnderlined green\x1b[0m \x1b[33;46mYellow on cyan\x1b[0m';
const result = stripAnsiCodes(complexText);
expect(result).toBe('Bold red Underlined green Yellow on cyan');
});
test('should handle non-string input gracefully', () => {
expect(stripAnsiCodes(null)).toBe(null);
expect(stripAnsiCodes(undefined)).toBe(undefined);
expect(stripAnsiCodes(123)).toBe(123);
expect(stripAnsiCodes({})).toEqual({});
});
test('should handle real chalk output patterns', () => {
// Test patterns similar to what chalk produces
const chalkLikeText =
'1 \x1b[32m✓ done\x1b[39m Setup Project \x1b[31m(high)\x1b[39m';
const result = stripAnsiCodes(chalkLikeText);
expect(result).toBe('1 ✓ done Setup Project (high)');
});
test('should handle multiline text with ANSI codes', () => {
const multilineText =
'\x1b[31mLine 1\x1b[0m\n\x1b[32mLine 2\x1b[0m\n\x1b[33mLine 3\x1b[0m';
const result = stripAnsiCodes(multilineText);
expect(result).toBe('Line 1\nLine 2\nLine 3');
});
});