@task-mcp/shared 1.0.29 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/schemas/inbox.d.ts +2 -2
- package/dist/schemas/response-format.d.ts +11 -0
- package/dist/schemas/response-format.d.ts.map +1 -1
- package/dist/schemas/response-format.js.map +1 -1
- package/dist/schemas/session.d.ts +12 -12
- package/dist/schemas/state.d.ts +2 -2
- package/dist/schemas/task.d.ts +9 -0
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +3 -0
- package/dist/schemas/task.js.map +1 -1
- package/dist/utils/clustering.d.ts +60 -0
- package/dist/utils/clustering.d.ts.map +1 -0
- package/dist/utils/clustering.js +283 -0
- package/dist/utils/clustering.js.map +1 -0
- package/dist/utils/clustering.test.d.ts +2 -0
- package/dist/utils/clustering.test.d.ts.map +1 -0
- package/dist/utils/clustering.test.js +237 -0
- package/dist/utils/clustering.test.js.map +1 -0
- package/dist/utils/env.d.ts +24 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +40 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/hierarchy.d.ts.map +1 -1
- package/dist/utils/hierarchy.js +7 -3
- package/dist/utils/hierarchy.js.map +1 -1
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/intent-extractor.d.ts +30 -0
- package/dist/utils/intent-extractor.d.ts.map +1 -0
- package/dist/utils/intent-extractor.js +135 -0
- package/dist/utils/intent-extractor.js.map +1 -0
- package/dist/utils/intent-extractor.test.d.ts +2 -0
- package/dist/utils/intent-extractor.test.d.ts.map +1 -0
- package/dist/utils/intent-extractor.test.js +69 -0
- package/dist/utils/intent-extractor.test.js.map +1 -0
- package/dist/utils/natural-language.d.ts.map +1 -1
- package/dist/utils/natural-language.js +9 -8
- package/dist/utils/natural-language.js.map +1 -1
- package/dist/utils/natural-language.test.js +22 -0
- package/dist/utils/natural-language.test.js.map +1 -1
- package/dist/utils/plan-parser.d.ts.map +1 -1
- package/dist/utils/plan-parser.js +2 -8
- package/dist/utils/plan-parser.js.map +1 -1
- package/dist/utils/projection.d.ts.map +1 -1
- package/dist/utils/projection.js +43 -1
- package/dist/utils/projection.js.map +1 -1
- package/dist/utils/projection.test.js +57 -7
- package/dist/utils/projection.test.js.map +1 -1
- package/dist/utils/terminal-ui.test.js +13 -22
- package/dist/utils/terminal-ui.test.js.map +1 -1
- package/package.json +1 -1
- package/src/schemas/response-format.ts +15 -2
- package/src/schemas/task.ts +3 -0
- package/src/utils/clustering.test.ts +285 -0
- package/src/utils/clustering.ts +336 -0
- package/src/utils/env.ts +41 -0
- package/src/utils/hierarchy.ts +9 -5
- package/src/utils/index.ts +17 -0
- package/src/utils/intent-extractor.test.ts +84 -0
- package/src/utils/intent-extractor.ts +156 -0
- package/src/utils/natural-language.test.ts +27 -0
- package/src/utils/natural-language.ts +10 -9
- package/src/utils/plan-parser.ts +4 -16
- package/src/utils/projection.test.ts +61 -7
- package/src/utils/projection.ts +44 -1
- package/src/utils/terminal-ui.test.ts +13 -22
|
@@ -255,6 +255,33 @@ describe("parseTaskInput", () => {
|
|
|
255
255
|
const result = parseTaskInput("간단한 작업 !medium #task");
|
|
256
256
|
expect(result.workContext).toBeUndefined();
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
test("parses done: before :: to prevent intent capturing done:", () => {
|
|
260
|
+
// Regression test: done: should be extracted before intent (::)
|
|
261
|
+
const result = parseTaskInput(
|
|
262
|
+
"API 리팩토링 :: 성능 개선 done:테스트통과,코드리뷰 @thorough #backend !high"
|
|
263
|
+
);
|
|
264
|
+
expect(result.title).toBe("API 리팩토링");
|
|
265
|
+
expect(result.workContext?.intent).toBe("성능 개선");
|
|
266
|
+
expect(result.workContext?.acceptanceCriteria).toEqual(["테스트통과", "코드리뷰"]);
|
|
267
|
+
expect(result.workContext?.qualityLevel).toBe("thorough");
|
|
268
|
+
expect(result.priority).toBe("high");
|
|
269
|
+
expect(result.tags).toEqual(["backend"]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("parses done: without intent", () => {
|
|
273
|
+
const result = parseTaskInput("테스트 작성 done:유닛테스트,통합테스트 !high");
|
|
274
|
+
expect(result.title).toBe("테스트 작성");
|
|
275
|
+
expect(result.workContext?.acceptanceCriteria).toEqual(["유닛테스트", "통합테스트"]);
|
|
276
|
+
expect(result.workContext?.intent).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("parses intent without done:", () => {
|
|
280
|
+
const result = parseTaskInput("리팩토링 :: 가독성 향상 !medium");
|
|
281
|
+
expect(result.title).toBe("리팩토링");
|
|
282
|
+
expect(result.workContext?.intent).toBe("가독성 향상");
|
|
283
|
+
expect(result.workContext?.acceptanceCriteria).toBeUndefined();
|
|
284
|
+
});
|
|
258
285
|
});
|
|
259
286
|
});
|
|
260
287
|
|
|
@@ -316,7 +316,16 @@ export function parseTaskInput(input: string): TaskCreateInput {
|
|
|
316
316
|
const result: TaskCreateInput = { title: "" };
|
|
317
317
|
const workContext: Partial<WorkContext> = {};
|
|
318
318
|
|
|
319
|
-
// Extract
|
|
319
|
+
// Extract acceptanceCriteria FIRST (done:xxx,yyy or done:xxx)
|
|
320
|
+
// Must be before intent extraction to prevent done: from being captured as intent
|
|
321
|
+
// Format: "done:criterion1,criterion2" or "done:single criterion"
|
|
322
|
+
const doneMatch = remaining.match(/done:([^\s@#!~^:]+(?:,[^\s@#!~^:]+)*)/i);
|
|
323
|
+
if (doneMatch) {
|
|
324
|
+
workContext.acceptanceCriteria = doneMatch[1]!.split(",").map((c) => c.trim());
|
|
325
|
+
remaining = remaining.replace(doneMatch[0], " ").trim();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Extract intent (:: separator)
|
|
320
329
|
// Format: "Task title :: intent description"
|
|
321
330
|
const intentMatch = remaining.match(/\s*::\s*(.+?)(?=\s+[@#!~^]|$)/);
|
|
322
331
|
if (intentMatch) {
|
|
@@ -324,14 +333,6 @@ export function parseTaskInput(input: string): TaskCreateInput {
|
|
|
324
333
|
remaining = remaining.replace(intentMatch[0], " ").trim();
|
|
325
334
|
}
|
|
326
335
|
|
|
327
|
-
// Extract acceptanceCriteria (done:xxx,yyy or done:xxx)
|
|
328
|
-
// Format: "done:criterion1,criterion2" or "done:single criterion"
|
|
329
|
-
const doneMatch = remaining.match(/done:([^\s@#!~^]+(?:,[^\s@#!~^]+)*)/i);
|
|
330
|
-
if (doneMatch) {
|
|
331
|
-
workContext.acceptanceCriteria = doneMatch[1]!.split(",").map((c) => c.trim());
|
|
332
|
-
remaining = remaining.replace(doneMatch[0], " ").trim();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
336
|
// Extract qualityLevel (@quick, @thorough, etc.) - before contexts
|
|
336
337
|
// These are reserved keywords that become qualityLevel, not contexts
|
|
337
338
|
const qualityLevelKeywords = Object.keys(QUALITY_LEVEL_KEYWORDS);
|
package/src/utils/plan-parser.ts
CHANGED
|
@@ -93,10 +93,7 @@ function preprocessInput(input: string): string {
|
|
|
93
93
|
* `);
|
|
94
94
|
* ```
|
|
95
95
|
*/
|
|
96
|
-
export function parsePlan(
|
|
97
|
-
input: string,
|
|
98
|
-
options: ParsePlanOptions = {}
|
|
99
|
-
): ParsePlanResult {
|
|
96
|
+
export function parsePlan(input: string, options: ParsePlanOptions = {}): ParsePlanResult {
|
|
100
97
|
if (input.length > MAX_PLAN_LENGTH) {
|
|
101
98
|
return {
|
|
102
99
|
tasks: [],
|
|
@@ -121,7 +118,6 @@ export function parsePlan(
|
|
|
121
118
|
let taskCounter = 0;
|
|
122
119
|
let currentPhase: { tempId: string; title: string } | null = null;
|
|
123
120
|
let currentSection: { tempId: string; level: number } | null = null;
|
|
124
|
-
let currentParent: { tempId: string; level: number; indent: number } | null = null;
|
|
125
121
|
const parentStack: Array<{ tempId: string; level: number; indent: number }> = [];
|
|
126
122
|
|
|
127
123
|
for (const line of lines) {
|
|
@@ -160,7 +156,6 @@ export function parsePlan(
|
|
|
160
156
|
|
|
161
157
|
// Reset section and parent stack
|
|
162
158
|
currentSection = null;
|
|
163
|
-
currentParent = null;
|
|
164
159
|
parentStack.length = 0;
|
|
165
160
|
continue;
|
|
166
161
|
}
|
|
@@ -192,7 +187,6 @@ export function parsePlan(
|
|
|
192
187
|
});
|
|
193
188
|
|
|
194
189
|
currentSection = { tempId, level: 1 };
|
|
195
|
-
currentParent = null;
|
|
196
190
|
parentStack.length = 0;
|
|
197
191
|
continue;
|
|
198
192
|
}
|
|
@@ -230,7 +224,6 @@ export function parsePlan(
|
|
|
230
224
|
|
|
231
225
|
// Extract metadata from content
|
|
232
226
|
let title = content;
|
|
233
|
-
let description: string | undefined;
|
|
234
227
|
let priority: Priority | undefined;
|
|
235
228
|
let tags: string[] | undefined;
|
|
236
229
|
let dependsOn: string[] | undefined;
|
|
@@ -252,7 +245,7 @@ export function parsePlan(
|
|
|
252
245
|
// Extract description from remaining parentheses (not dependency-related)
|
|
253
246
|
const descExtracted = extractDescription(title);
|
|
254
247
|
title = descExtracted.title;
|
|
255
|
-
description = descExtracted.description;
|
|
248
|
+
const description = descExtracted.description;
|
|
256
249
|
|
|
257
250
|
const task: ParsedPlanTask = {
|
|
258
251
|
title,
|
|
@@ -269,7 +262,6 @@ export function parsePlan(
|
|
|
269
262
|
|
|
270
263
|
// Push to parent stack for potential children
|
|
271
264
|
parentStack.push({ tempId, level, indent });
|
|
272
|
-
currentParent = { tempId, level, indent };
|
|
273
265
|
continue;
|
|
274
266
|
}
|
|
275
267
|
|
|
@@ -303,7 +295,6 @@ export function parsePlan(
|
|
|
303
295
|
}
|
|
304
296
|
|
|
305
297
|
let title = content;
|
|
306
|
-
let description: string | undefined;
|
|
307
298
|
let priority: Priority | undefined;
|
|
308
299
|
let tags: string[] | undefined;
|
|
309
300
|
let dependsOn: string[] | undefined;
|
|
@@ -323,7 +314,7 @@ export function parsePlan(
|
|
|
323
314
|
// Extract description from remaining parentheses
|
|
324
315
|
const descExtracted = extractDescription(title);
|
|
325
316
|
title = descExtracted.title;
|
|
326
|
-
description = descExtracted.description;
|
|
317
|
+
const description = descExtracted.description;
|
|
327
318
|
|
|
328
319
|
tasks.push({
|
|
329
320
|
title,
|
|
@@ -347,10 +338,7 @@ export function parsePlan(
|
|
|
347
338
|
/**
|
|
348
339
|
* Infer dependencies from text patterns like "depends on X", "after X"
|
|
349
340
|
*/
|
|
350
|
-
function inferDependenciesFromText(
|
|
351
|
-
content: string,
|
|
352
|
-
existingTasks: ParsedPlanTask[]
|
|
353
|
-
): string[] {
|
|
341
|
+
function inferDependenciesFromText(content: string, existingTasks: ParsedPlanTask[]): string[] {
|
|
354
342
|
const deps: string[] = [];
|
|
355
343
|
const lower = content.toLowerCase();
|
|
356
344
|
|
|
@@ -45,7 +45,7 @@ const createInboxItem = (overrides: Partial<InboxItem> = {}): InboxItem => ({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe("formatTask", () => {
|
|
48
|
-
test("concise format returns
|
|
48
|
+
test("concise format returns essential fields plus description for context", () => {
|
|
49
49
|
const task = createTask();
|
|
50
50
|
const result = formatTask(task, "concise");
|
|
51
51
|
|
|
@@ -54,11 +54,12 @@ describe("formatTask", () => {
|
|
|
54
54
|
title: "Test Task",
|
|
55
55
|
status: "pending",
|
|
56
56
|
priority: "medium",
|
|
57
|
+
description: "This is a test task with a long description",
|
|
57
58
|
});
|
|
58
|
-
expect(Object.keys(result)).toHaveLength(
|
|
59
|
+
expect(Object.keys(result)).toHaveLength(5);
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
test("standard format returns
|
|
62
|
+
test("standard format returns common fields including description", () => {
|
|
62
63
|
const task = createTask();
|
|
63
64
|
const result = formatTask(task, "standard");
|
|
64
65
|
|
|
@@ -67,6 +68,7 @@ describe("formatTask", () => {
|
|
|
67
68
|
title: "Test Task",
|
|
68
69
|
status: "pending",
|
|
69
70
|
priority: "medium",
|
|
71
|
+
description: "This is a test task with a long description",
|
|
70
72
|
dueDate: "2025-01-15",
|
|
71
73
|
tags: ["test", "unit"],
|
|
72
74
|
contexts: ["work"],
|
|
@@ -94,6 +96,57 @@ describe("formatTask", () => {
|
|
|
94
96
|
expect(result).not.toHaveProperty("dueDate");
|
|
95
97
|
expect(result).not.toHaveProperty("tags");
|
|
96
98
|
});
|
|
99
|
+
|
|
100
|
+
test("concise format includes workContext fields (intent, methodology, quality, patterns, criteria)", () => {
|
|
101
|
+
const task = createTask({
|
|
102
|
+
workContext: {
|
|
103
|
+
intent: "Migrate API layer to service pattern",
|
|
104
|
+
methodology: "TDD + iterative refactoring",
|
|
105
|
+
qualityLevel: "thorough",
|
|
106
|
+
patterns: {
|
|
107
|
+
parallelExecution: true,
|
|
108
|
+
testFirst: true,
|
|
109
|
+
},
|
|
110
|
+
acceptanceCriteria: ["All tests pass", "No type errors"],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const result = formatTask(task, "concise");
|
|
114
|
+
|
|
115
|
+
expect(result).toHaveProperty("intent", "Migrate API layer to service pattern");
|
|
116
|
+
expect(result).toHaveProperty("methodology", "TDD + iterative refactoring");
|
|
117
|
+
expect(result).toHaveProperty("quality", "thorough");
|
|
118
|
+
expect(result).toHaveProperty("patterns", { parallelExecution: true, testFirst: true });
|
|
119
|
+
expect(result).toHaveProperty("criteria", ["All tests pass", "No type errors"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("standard format includes full workContext with patterns and criteria", () => {
|
|
123
|
+
const task = createTask({
|
|
124
|
+
workContext: {
|
|
125
|
+
intent: "Refactor module",
|
|
126
|
+
methodology: "Analyze → Extract → Test",
|
|
127
|
+
qualityLevel: "standard",
|
|
128
|
+
patterns: {
|
|
129
|
+
parallelExecution: true,
|
|
130
|
+
testFirst: true,
|
|
131
|
+
reviewRequired: false,
|
|
132
|
+
},
|
|
133
|
+
acceptanceCriteria: ["Tests pass", "No regressions"],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const result = formatTask(task, "standard");
|
|
137
|
+
|
|
138
|
+
expect(result).toHaveProperty("workContext");
|
|
139
|
+
const wc = (result as { workContext?: unknown }).workContext as Record<string, unknown>;
|
|
140
|
+
expect(wc["intent"]).toBe("Refactor module");
|
|
141
|
+
expect(wc["methodology"]).toBe("Analyze → Extract → Test");
|
|
142
|
+
expect(wc["qualityLevel"]).toBe("standard");
|
|
143
|
+
expect(wc["patterns"]).toEqual({
|
|
144
|
+
parallelExecution: true,
|
|
145
|
+
testFirst: true,
|
|
146
|
+
reviewRequired: false,
|
|
147
|
+
});
|
|
148
|
+
expect(wc["acceptanceCriteria"]).toEqual(["Tests pass", "No regressions"]);
|
|
149
|
+
});
|
|
97
150
|
});
|
|
98
151
|
|
|
99
152
|
describe("formatTasks", () => {
|
|
@@ -107,6 +160,7 @@ describe("formatTasks", () => {
|
|
|
107
160
|
title: "Test Task",
|
|
108
161
|
status: "pending",
|
|
109
162
|
priority: "medium",
|
|
163
|
+
description: "This is a test task with a long description",
|
|
110
164
|
});
|
|
111
165
|
});
|
|
112
166
|
|
|
@@ -404,8 +458,8 @@ describe("token efficiency", () => {
|
|
|
404
458
|
const conciseSize = JSON.stringify(concise).length;
|
|
405
459
|
const fullSize = JSON.stringify(full).length;
|
|
406
460
|
|
|
407
|
-
// Concise should be at least
|
|
408
|
-
expect(conciseSize).toBeLessThan(fullSize * 0.
|
|
461
|
+
// Concise should be at least 40% smaller (now includes description for session context)
|
|
462
|
+
expect(conciseSize).toBeLessThan(fullSize * 0.6);
|
|
409
463
|
});
|
|
410
464
|
|
|
411
465
|
test("concise list is significantly smaller than full list", () => {
|
|
@@ -424,7 +478,7 @@ describe("token efficiency", () => {
|
|
|
424
478
|
const conciseSize = JSON.stringify(concise).length;
|
|
425
479
|
const fullSize = JSON.stringify(full).length;
|
|
426
480
|
|
|
427
|
-
// For list of 20 tasks, concise should be
|
|
428
|
-
expect(conciseSize).toBeLessThan(fullSize * 0.
|
|
481
|
+
// For list of 20 tasks, concise should be 40%+ smaller (now includes description)
|
|
482
|
+
expect(conciseSize).toBeLessThan(fullSize * 0.6);
|
|
429
483
|
});
|
|
430
484
|
});
|
package/src/utils/projection.ts
CHANGED
|
@@ -36,13 +36,43 @@ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | Ta
|
|
|
36
36
|
status: task.status,
|
|
37
37
|
priority: task.priority,
|
|
38
38
|
};
|
|
39
|
-
// Include
|
|
39
|
+
// Include description for session context (truncated for token efficiency)
|
|
40
|
+
// Skip if intent exists and provides enough context
|
|
41
|
+
if (task.description) {
|
|
42
|
+
const hasIntent = !!task.workContext?.intent;
|
|
43
|
+
// With intent: shorter description (100 chars), without: fuller context (200 chars)
|
|
44
|
+
const maxLen = hasIntent ? 100 : 200;
|
|
45
|
+
summary.description = truncate(task.description, maxLen);
|
|
46
|
+
}
|
|
47
|
+
// Include workContext fields for session resumption
|
|
40
48
|
if (task.workContext?.intent) {
|
|
41
49
|
summary.intent = task.workContext.intent;
|
|
42
50
|
}
|
|
51
|
+
if (task.workContext?.methodology) {
|
|
52
|
+
summary.methodology = task.workContext.methodology;
|
|
53
|
+
}
|
|
43
54
|
if (task.workContext?.qualityLevel) {
|
|
44
55
|
summary.quality = task.workContext.qualityLevel;
|
|
45
56
|
}
|
|
57
|
+
// Include patterns for execution guidance
|
|
58
|
+
if (task.workContext?.patterns) {
|
|
59
|
+
const p = task.workContext.patterns;
|
|
60
|
+
const patterns: {
|
|
61
|
+
parallelExecution?: boolean;
|
|
62
|
+
testFirst?: boolean;
|
|
63
|
+
reviewRequired?: boolean;
|
|
64
|
+
} = {};
|
|
65
|
+
if (p.parallelExecution !== undefined) patterns.parallelExecution = p.parallelExecution;
|
|
66
|
+
if (p.testFirst !== undefined) patterns.testFirst = p.testFirst;
|
|
67
|
+
if (p.reviewRequired !== undefined) patterns.reviewRequired = p.reviewRequired;
|
|
68
|
+
if (Object.keys(patterns).length > 0) {
|
|
69
|
+
summary.patterns = patterns;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Include acceptance criteria for completion validation
|
|
73
|
+
if (task.workContext?.acceptanceCriteria?.length) {
|
|
74
|
+
summary.criteria = task.workContext.acceptanceCriteria;
|
|
75
|
+
}
|
|
46
76
|
return summary;
|
|
47
77
|
}
|
|
48
78
|
|
|
@@ -53,6 +83,7 @@ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | Ta
|
|
|
53
83
|
title: task.title,
|
|
54
84
|
status: task.status,
|
|
55
85
|
priority: task.priority,
|
|
86
|
+
...(task.description !== undefined && { description: task.description }),
|
|
56
87
|
...(task.dueDate !== undefined && { dueDate: task.dueDate }),
|
|
57
88
|
...(task.tags !== undefined && { tags: task.tags }),
|
|
58
89
|
...(task.contexts !== undefined && { contexts: task.contexts }),
|
|
@@ -65,6 +96,18 @@ export function formatTask(task: Task, format: ResponseFormat): TaskSummary | Ta
|
|
|
65
96
|
...(wc.intent !== undefined && { intent: wc.intent }),
|
|
66
97
|
...(wc.methodology !== undefined && { methodology: wc.methodology }),
|
|
67
98
|
...(wc.qualityLevel !== undefined && { qualityLevel: wc.qualityLevel }),
|
|
99
|
+
...(wc.patterns !== undefined && {
|
|
100
|
+
patterns: {
|
|
101
|
+
...(wc.patterns.parallelExecution !== undefined && {
|
|
102
|
+
parallelExecution: wc.patterns.parallelExecution,
|
|
103
|
+
}),
|
|
104
|
+
...(wc.patterns.testFirst !== undefined && { testFirst: wc.patterns.testFirst }),
|
|
105
|
+
...(wc.patterns.reviewRequired !== undefined && {
|
|
106
|
+
reviewRequired: wc.patterns.reviewRequired,
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
...(wc.acceptanceCriteria?.length && { acceptanceCriteria: wc.acceptanceCriteria }),
|
|
68
111
|
};
|
|
69
112
|
}
|
|
70
113
|
return result;
|
|
@@ -917,16 +917,13 @@ describe("formatPriorityBadge", () => {
|
|
|
917
917
|
|
|
918
918
|
describe("renderSection", () => {
|
|
919
919
|
test("renders header and items", () => {
|
|
920
|
-
const items = [
|
|
921
|
-
{ primary: "Task 1" },
|
|
922
|
-
{ primary: "Task 2" },
|
|
923
|
-
];
|
|
920
|
+
const items = [{ primary: "Task 1" }, { primary: "Task 2" }];
|
|
924
921
|
const lines = renderSection("Test Section", items);
|
|
925
922
|
|
|
926
923
|
expect(lines.length).toBeGreaterThan(0);
|
|
927
924
|
expect(stripAnsi(lines[0] ?? "")).toContain("Test Section");
|
|
928
|
-
expect(lines.some(l => l.includes("Task 1"))).toBe(true);
|
|
929
|
-
expect(lines.some(l => l.includes("Task 2"))).toBe(true);
|
|
925
|
+
expect(lines.some((l) => l.includes("Task 1"))).toBe(true);
|
|
926
|
+
expect(lines.some((l) => l.includes("Task 2"))).toBe(true);
|
|
930
927
|
});
|
|
931
928
|
|
|
932
929
|
test("renders with icon", () => {
|
|
@@ -936,25 +933,21 @@ describe("renderSection", () => {
|
|
|
936
933
|
|
|
937
934
|
test("renders empty state message", () => {
|
|
938
935
|
const lines = renderSection("Empty", [], { emptyMessage: "No items" });
|
|
939
|
-
expect(lines.some(l => stripAnsi(l).includes("No items"))).toBe(true);
|
|
936
|
+
expect(lines.some((l) => stripAnsi(l).includes("No items"))).toBe(true);
|
|
940
937
|
});
|
|
941
938
|
|
|
942
939
|
test("renders item details", () => {
|
|
943
|
-
const items = [
|
|
944
|
-
{ primary: "Task", details: ["Detail 1", "Detail 2"] },
|
|
945
|
-
];
|
|
940
|
+
const items = [{ primary: "Task", details: ["Detail 1", "Detail 2"] }];
|
|
946
941
|
const lines = renderSection("With Details", items);
|
|
947
|
-
expect(lines.some(l => l.includes("Detail 1"))).toBe(true);
|
|
948
|
-
expect(lines.some(l => l.includes("Detail 2"))).toBe(true);
|
|
942
|
+
expect(lines.some((l) => l.includes("Detail 1"))).toBe(true);
|
|
943
|
+
expect(lines.some((l) => l.includes("Detail 2"))).toBe(true);
|
|
949
944
|
});
|
|
950
945
|
|
|
951
946
|
test("renders prefix and suffix", () => {
|
|
952
|
-
const items = [
|
|
953
|
-
{ primary: "Task", prefix: "[x]", suffix: "P1" },
|
|
954
|
-
];
|
|
947
|
+
const items = [{ primary: "Task", prefix: "[x]", suffix: "P1" }];
|
|
955
948
|
const lines = renderSection("Test", items);
|
|
956
|
-
expect(lines.some(l => l.includes("[x]"))).toBe(true);
|
|
957
|
-
expect(lines.some(l => l.includes("P1"))).toBe(true);
|
|
949
|
+
expect(lines.some((l) => l.includes("[x]"))).toBe(true);
|
|
950
|
+
expect(lines.some((l) => l.includes("P1"))).toBe(true);
|
|
958
951
|
});
|
|
959
952
|
});
|
|
960
953
|
|
|
@@ -966,8 +959,8 @@ describe("renderSections", () => {
|
|
|
966
959
|
];
|
|
967
960
|
const lines = renderSections(sections);
|
|
968
961
|
|
|
969
|
-
expect(lines.some(l => stripAnsi(l).includes("Section 1"))).toBe(true);
|
|
970
|
-
expect(lines.some(l => stripAnsi(l).includes("Section 2"))).toBe(true);
|
|
962
|
+
expect(lines.some((l) => stripAnsi(l).includes("Section 1"))).toBe(true);
|
|
963
|
+
expect(lines.some((l) => stripAnsi(l).includes("Section 2"))).toBe(true);
|
|
971
964
|
expect(lines.includes("")).toBe(true); // spacing between sections
|
|
972
965
|
});
|
|
973
966
|
});
|
|
@@ -1013,9 +1006,7 @@ describe("renderStats", () => {
|
|
|
1013
1006
|
});
|
|
1014
1007
|
|
|
1015
1008
|
test("applies color to values", () => {
|
|
1016
|
-
const stats = [
|
|
1017
|
-
{ label: "Blocked", value: 3, color: "red" as const },
|
|
1018
|
-
];
|
|
1009
|
+
const stats = [{ label: "Blocked", value: 3, color: "red" as const }];
|
|
1019
1010
|
const result = renderStats(stats);
|
|
1020
1011
|
expect(result).toContain("\x1b[31m"); // red
|
|
1021
1012
|
});
|