-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix: return correct target tag in add_task response #1669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /** | ||
| * Tests for handleApiResult tag field behavior | ||
| * | ||
| * Regression test for https://github.com/eyaltoledano/claude-task-master/issues/1638 | ||
| * Bug: handleApiResult returns the active tag from state.json instead of the | ||
| * resolved target tag when a tag is explicitly provided to the operation. | ||
| */ | ||
|
|
||
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| import { handleApiResult } from './utils.js'; | ||
|
|
||
| // Mock fs and path to control getCurrentTag behavior | ||
| vi.mock('node:fs', () => ({ | ||
| default: { | ||
| existsSync: vi.fn(() => true), | ||
| readFileSync: vi.fn(() => | ||
| JSON.stringify({ currentTag: 'active-tag-from-state' }) | ||
| ) | ||
| } | ||
| })); | ||
|
|
||
| // Mock the package.json import | ||
| vi.mock('../../../../package.json', () => ({ | ||
| default: { version: '0.0.0-test', name: 'task-master-ai' } | ||
| })); | ||
|
|
||
| describe('handleApiResult tag field', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('should use provided tag instead of reading from state.json', async () => { | ||
| const result = await handleApiResult({ | ||
| result: { | ||
| success: true, | ||
| data: { taskId: 5, message: 'Success' } | ||
| }, | ||
| projectRoot: '/mock/project', | ||
| tag: 'target-tag' | ||
| }); | ||
|
|
||
| const responseText = (result.content[0] as { type: 'text'; text: string }).text; | ||
| const parsed = JSON.parse(responseText); | ||
|
|
||
| // The tag field should be the explicitly provided tag, not the active tag | ||
| expect(parsed.tag).toBe('target-tag'); | ||
| }); | ||
|
|
||
| it('should fall back to state.json tag when no tag is provided', async () => { | ||
| const result = await handleApiResult({ | ||
| result: { | ||
| success: true, | ||
| data: { taskId: 5, message: 'Success' } | ||
| }, | ||
| projectRoot: '/mock/project' | ||
| }); | ||
|
|
||
| const responseText = (result.content[0] as { type: 'text'; text: string }).text; | ||
| const parsed = JSON.parse(responseText); | ||
|
|
||
| // Without explicit tag, it should read from state.json | ||
| expect(parsed.tag).toBe('active-tag-from-state'); | ||
| }); | ||
|
|
||
| it('should not include tag field when neither tag nor projectRoot is provided', async () => { | ||
| const result = await handleApiResult({ | ||
| result: { | ||
| success: true, | ||
| data: { taskId: 5, message: 'Success' } | ||
| } | ||
| }); | ||
|
|
||
| const responseText = (result.content[0] as { type: 'text'; text: string }).text; | ||
| const parsed = JSON.parse(responseText); | ||
|
|
||
| expect(parsed.tag).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should use provided tag in error responses too', async () => { | ||
| const result = await handleApiResult({ | ||
| result: { | ||
| success: false, | ||
| error: { message: 'Something went wrong' } | ||
| }, | ||
| projectRoot: '/mock/project', | ||
| tag: 'target-tag' | ||
| }); | ||
|
|
||
| // Error responses include tag in text format | ||
| const responseText = (result.content[0] as { type: 'text'; text: string }).text; | ||
| expect(responseText).toContain('Current Tag: target-tag'); | ||
| expect(responseText).not.toContain('active-tag-from-state'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -119,7 +119,7 @@ export interface TaskImplementationMetadata { | |||||
| * Placeholder task interface for temporary/minimal task objects | ||||||
| */ | ||||||
| export interface PlaceholderTask { | ||||||
| id: string; | ||||||
| id: number | string; | ||||||
| title: string; | ||||||
| status: TaskStatus; | ||||||
| priority: TaskPriority; | ||||||
|
|
@@ -129,7 +129,7 @@ export interface PlaceholderTask { | |||||
| * Base task interface | ||||||
| */ | ||||||
| export interface Task extends TaskImplementationMetadata { | ||||||
| id: string; | ||||||
| id: number | string; | ||||||
| title: string; | ||||||
| description: string; | ||||||
| status: TaskStatus; | ||||||
|
Comment on lines
131
to
135
|
||||||
|
|
@@ -171,7 +171,7 @@ export interface Task extends TaskImplementationMetadata { | |||||
| */ | ||||||
| export interface Subtask extends Omit<Task, 'id' | 'subtasks'> { | ||||||
| id: number | string; | ||||||
| parentId: string; | ||||||
| parentId: number | string; | ||||||
|
||||||
| parentId: number | string; | |
| parentId: string; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -282,19 +282,19 @@ export class FileStorage implements IStorage { | |
| } | ||
|
|
||
| /** | ||
| * Normalize task IDs - keep Task IDs as strings, Subtask IDs as numbers | ||
| * Normalize task IDs - Task IDs and Subtask IDs are both numbers for file storage | ||
| * Note: Uses spread operator to preserve all task properties including user-defined metadata | ||
| */ | ||
| private normalizeTaskIds(tasks: Task[]): Task[] { | ||
|
Comment on lines
284
to
288
|
||
| return tasks.map((task) => ({ | ||
| ...task, | ||
| id: String(task.id), // Task IDs are strings | ||
| id: Number(task.id), // Task IDs are numbers | ||
| dependencies: task.dependencies?.map((dep) => String(dep)) || [], | ||
| subtasks: | ||
| task.subtasks?.map((subtask) => ({ | ||
| ...subtask, | ||
| id: Number(subtask.id), // Subtask IDs are numbers | ||
| parentId: String(subtask.parentId) // Parent ID is string (Task ID) | ||
| parentId: Number(subtask.parentId) // Parent ID is number (Task ID) | ||
| })) || [] | ||
| })); | ||
| } | ||
|
|
@@ -404,7 +404,7 @@ export class FileStorage implements IStorage { | |
| ...existingTask, | ||
| ...updates, | ||
| ...(mergedSubtasks && { subtasks: mergedSubtasks }), | ||
| id: String(taskId) // Keep consistent with normalizeTaskIds | ||
| id: Number(taskId) // Keep consistent with normalizeTaskIds | ||
| }; | ||
| await this.saveTasks(tasks, tag); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -217,18 +217,18 @@ export class FormatHandler { | |||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Normalize task IDs - keep Task IDs as strings, Subtask IDs as numbers | ||||||
| * Normalize task IDs - Task IDs and Subtask IDs are both numbers for file storage | ||||||
|
||||||
| * Normalize task IDs - Task IDs and Subtask IDs are both numbers for file storage | |
| * Normalize task and subtask IDs to numbers for file storage (dependency IDs remain strings) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -54,7 +54,7 @@ export function createTask( | |||||
| overrides: Partial<Omit<Task, 'id'>> & { id: number | string; title: string } | ||||||
| ): Task { | ||||||
| return { | ||||||
| id: String(overrides.id), | ||||||
| id: Number(overrides.id), | ||||||
|
||||||
| id: Number(overrides.id), | |
| id: String(overrides.id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description/title indicates the add_task response tag bug is fixed by passing the resolved target tag into handleApiResult, but in the current codebase the add_task tool (mcp-server/src/tools/add-task.js) still calls handleApiResult without supplying
tag: resolvedTag, so responses will continue to fall back to state.json. These tests only validate handleApiResult’s precedence rules, not that add_task actually passes the resolved tag—please add the call-site change (and ideally a tool-level regression test) so the reported bug is truly fixed.