@opentask/taskin-task-manager 3.0.2 → 3.2.0
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/CHANGELOG.md +119 -0
- package/dist/completion-blockers.test.d.ts +1 -0
- package/dist/completion-blockers.test.js +57 -0
- package/dist/completion-blockers.test.js.map +1 -0
- package/dist/filter-tasks/filter-criteria.d.ts +181 -0
- package/dist/filter-tasks/filter-criteria.js +115 -0
- package/dist/filter-tasks/filter-criteria.js.map +1 -0
- package/dist/filter-tasks/filter-criteria.test.d.ts +1 -0
- package/dist/filter-tasks/filter-criteria.test.js +86 -0
- package/dist/filter-tasks/filter-criteria.test.js.map +1 -0
- package/dist/filter-tasks/filter-tasks.d.ts +26 -0
- package/dist/filter-tasks/filter-tasks.js +99 -0
- package/dist/filter-tasks/filter-tasks.js.map +1 -0
- package/dist/filter-tasks/filter-tasks.test.d.ts +1 -0
- package/dist/filter-tasks/filter-tasks.test.js +110 -0
- package/dist/filter-tasks/filter-tasks.test.js.map +1 -0
- package/dist/filter-tasks/filter-tasks.types.d.ts +27 -0
- package/dist/filter-tasks/filter-tasks.types.js +2 -0
- package/dist/filter-tasks/filter-tasks.types.js.map +1 -0
- package/dist/filter-tasks/index.d.ts +4 -0
- package/dist/filter-tasks/index.js +3 -0
- package/dist/filter-tasks/index.js.map +1 -0
- package/dist/group-registry.contract.d.ts +18 -0
- package/dist/group-registry.contract.js +73 -0
- package/dist/group-registry.contract.js.map +1 -0
- package/dist/group-registry.types.d.ts +44 -0
- package/dist/group-registry.types.js +2 -0
- package/dist/group-registry.types.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/numerar-prioridade/index.d.ts +1 -0
- package/dist/numerar-prioridade/index.js +2 -0
- package/dist/numerar-prioridade/index.js.map +1 -0
- package/dist/numerar-prioridade/numerar-prioridade.d.ts +36 -0
- package/dist/numerar-prioridade/numerar-prioridade.js +75 -0
- package/dist/numerar-prioridade/numerar-prioridade.js.map +1 -0
- package/dist/numerar-prioridade/numerar-prioridade.test.d.ts +1 -0
- package/dist/numerar-prioridade/numerar-prioridade.test.js +55 -0
- package/dist/numerar-prioridade/numerar-prioridade.test.js.map +1 -0
- package/dist/ordenar-tarefas/index.d.ts +1 -0
- package/dist/ordenar-tarefas/index.js +2 -0
- package/dist/ordenar-tarefas/index.js.map +1 -0
- package/dist/ordenar-tarefas/ordenar-tarefas.d.ts +70 -0
- package/dist/ordenar-tarefas/ordenar-tarefas.js +79 -0
- package/dist/ordenar-tarefas/ordenar-tarefas.js.map +1 -0
- package/dist/ordenar-tarefas/ordenar-tarefas.test.d.ts +1 -0
- package/dist/ordenar-tarefas/ordenar-tarefas.test.js +70 -0
- package/dist/ordenar-tarefas/ordenar-tarefas.test.js.map +1 -0
- package/dist/task-manager.d.ts +34 -1
- package/dist/task-manager.js +58 -0
- package/dist/task-manager.js.map +1 -1
- package/dist/task-manager.types.d.ts +93 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +9 -0
- package/dist/testing.js.map +1 -0
- package/dist/user-registry.contract.js +6 -6
- package/dist/user-registry.contract.js.map +1 -1
- package/package.json +7 -6
- package/src/completion-blockers.test.ts +77 -0
- package/src/filter-tasks/filter-criteria.test.ts +113 -0
- package/src/filter-tasks/filter-criteria.ts +175 -0
- package/src/filter-tasks/filter-tasks.test.ts +129 -0
- package/src/filter-tasks/filter-tasks.ts +106 -0
- package/src/filter-tasks/filter-tasks.types.ts +26 -0
- package/src/filter-tasks/index.ts +16 -0
- package/src/group-registry.contract.ts +99 -0
- package/src/group-registry.types.ts +47 -0
- package/src/index.ts +4 -0
- package/src/numerar-prioridade/index.ts +1 -0
- package/src/numerar-prioridade/numerar-prioridade.test.ts +69 -0
- package/src/numerar-prioridade/numerar-prioridade.ts +80 -0
- package/src/ordenar-tarefas/index.ts +1 -0
- package/src/ordenar-tarefas/ordenar-tarefas.test.ts +92 -0
- package/src/ordenar-tarefas/ordenar-tarefas.ts +135 -0
- package/src/task-manager.ts +70 -0
- package/src/task-manager.types.ts +95 -0
- package/src/testing.ts +8 -0
- package/src/user-registry.contract.ts +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-registry.contract.js","sourceRoot":"","sources":["../src/user-registry.contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAG9C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,4BAA4B,CAAC,aAA2C;IACtF,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACtC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;YAClC,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;gBACtD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;gBAClE,MAAM,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC9B,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAChD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;gBACxD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;YACtD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;YAC3B,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;gBACpD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,QAAQ,CAAC,QAAQ,CAAC;oBACtB,EAAE,EAAE,YAAY;oBAChB,IAAI,EAAE,YAAY;oBAClB,KAAK,EAAE,kBAAkB;iBAC1B,CAAC,CAAC;gBAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;gBAChD,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBACpC,MAAM,CAAC,IAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"user-registry.contract.js","sourceRoot":"","sources":["../src/user-registry.contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAG9C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,4BAA4B,CAAC,aAA2C;IACtF,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACtC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;YAClC,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;gBACtD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;gBAClE,MAAM,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC9B,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAChD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;gBACxD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;YACtD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;YAC3B,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;gBACpD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,QAAQ,CAAC,QAAQ,CAAC;oBACtB,EAAE,EAAE,YAAY;oBAChB,IAAI,EAAE,YAAY;oBAClB,KAAK,EAAE,kBAAkB;iBAC1B,CAAC,CAAC;gBAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;gBAChD,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBACpC,MAAM,CAAC,IAAK,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;YACrD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;gBAC7D,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,QAAQ,CAAC,QAAQ,CAAC;oBACtB,EAAE,EAAE,OAAO;oBACX,IAAI,EAAE,aAAa;oBACnB,KAAK,EAAE,mBAAmB;iBAC3B,CAAC,CAAC;gBAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;gBACjD,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC/B,MAAM,CAAC,IAAK,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;YACrD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;gBACxD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;gBAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;YAC/B,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;YACnC,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;gBAC/D,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;gBAEpD,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC/B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACjC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;gBAC9C,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;YACpD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;gBAC1E,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;gBACpD,MAAM,KAAK,GAAG,QAAQ,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;gBAClD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACtD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;YACjC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;gBACtE,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAAC;oBAC5C,IAAI,EAAE,WAAW;oBACjB,KAAK,EAAE,eAAe;iBACvB,CAAC,CAAC;gBAEH,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBAClC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;gBAC9D,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,QAAQ,CAAC,QAAQ,CAAC;oBACtB,EAAE,EAAE,OAAO;oBACX,IAAI,EAAE,aAAa;oBACnB,KAAK,EAAE,mBAAmB;iBAC3B,CAAC,CAAC;gBAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAAC;oBAC5C,IAAI,EAAE,aAAa;oBACnB,KAAK,EAAE,mBAAmB;iBAC3B,CAAC,CAAC;gBAEH,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC9B,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACjD,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;gBAC7E,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,QAAQ,CAAC,QAAQ,CAAC;oBACtB,EAAE,EAAE,MAAM;oBACV,IAAI,EAAE,MAAM;oBACZ,KAAK,EAAE,kBAAkB;iBAC1B,CAAC,CAAC;gBAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAAC;oBAC5C,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,mBAAmB;iBAC3B,CAAC,CAAC;gBAEH,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC9B,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACjD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;YAC3B,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;gBAC7C,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBAClE,MAAM,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBAElE,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;gBAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3E,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;gBACvD,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;gBACvC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC7C,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentask/taskin-task-manager",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Task lifecycle management for Taskin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"motivation": "To manage the lifecycle of tasks, including creation, status updates, and retrieval.",
|
|
@@ -33,14 +33,15 @@
|
|
|
33
33
|
"default": "./dist/index.js"
|
|
34
34
|
},
|
|
35
35
|
"./testing": {
|
|
36
|
-
"types": "./dist/
|
|
37
|
-
"import": "./dist/
|
|
38
|
-
"require": "./dist/
|
|
39
|
-
"default": "./dist/
|
|
36
|
+
"types": "./dist/testing.d.ts",
|
|
37
|
+
"import": "./dist/testing.js",
|
|
38
|
+
"require": "./dist/testing.js",
|
|
39
|
+
"default": "./dist/testing.js"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"
|
|
43
|
+
"zod": "^4.5.4",
|
|
44
|
+
"@opentask/taskin-types": "2.3.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"typescript": "^6.0.0"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseTaskId, type Task } from '@opentask/taskin-types';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { TaskManager } from './task-manager.js';
|
|
4
|
+
import type { ITaskProvider } from './task-manager.types.js';
|
|
5
|
+
|
|
6
|
+
const tarefa = (extra: Partial<Task> = {}): Task =>
|
|
7
|
+
({ id: parseTaskId('001'), title: 'Uma tarefa', status: 'in-progress', type: 'feat', ...extra }) as Task;
|
|
8
|
+
|
|
9
|
+
function provider(overrides: Partial<ITaskProvider> = {}): ITaskProvider {
|
|
10
|
+
return {
|
|
11
|
+
initialize: vi.fn(async () => {}),
|
|
12
|
+
findTask: vi.fn(async () => tarefa()),
|
|
13
|
+
getAllTasks: vi.fn(async () => [tarefa()]),
|
|
14
|
+
updateTask: vi.fn(async () => {}),
|
|
15
|
+
createTask: vi.fn(),
|
|
16
|
+
lint: vi.fn(),
|
|
17
|
+
...overrides,
|
|
18
|
+
} as unknown as ITaskProvider;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* O aviso no `finish`, e por que ele **avisa** em vez de recusar.
|
|
23
|
+
*
|
|
24
|
+
* Fechar uma tarefa e um gesto que acontece uma vez, muitas vezes com pressa.
|
|
25
|
+
* Recusar ali torna o comando fragil e ensina a contornar. O portao duro vive no
|
|
26
|
+
* `lint` (task-075), que roda em CI e quebra o build; aqui o papel e outro —
|
|
27
|
+
* dizer, no momento em que a pessoa ainda esta olhando, o que ficou para tras.
|
|
28
|
+
*
|
|
29
|
+
* E uma **capacidade opcional** do provider: nem toda fonte tem checklist. Um
|
|
30
|
+
* provider sem ela conclui sem portao nenhum, em vez de receber uma chamada que
|
|
31
|
+
* falha.
|
|
32
|
+
*/
|
|
33
|
+
describe('finishTask e os criterios em aberto', () => {
|
|
34
|
+
it('conclui em silencio quando o provider nao tem a capacidade', async () => {
|
|
35
|
+
const manager = new TaskManager(provider());
|
|
36
|
+
|
|
37
|
+
const { blockers } = await manager.finishTaskComRelato(parseTaskId('001'));
|
|
38
|
+
|
|
39
|
+
expect(blockers).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('relata os itens em aberto que o provider aponta', async () => {
|
|
43
|
+
const manager = new TaskManager(
|
|
44
|
+
provider({
|
|
45
|
+
getCompletionBlockers: vi.fn(async () => [{ texto: 'Documentar', linha: 12 }]),
|
|
46
|
+
} as Partial<ITaskProvider>),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const { blockers } = await manager.finishTaskComRelato(parseTaskId('001'));
|
|
50
|
+
|
|
51
|
+
expect(blockers).toHaveLength(1);
|
|
52
|
+
expect(blockers[0]?.texto).toBe('Documentar');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('conclui mesmo assim: avisa, e nao recusa', async () => {
|
|
56
|
+
const updateTask = vi.fn(async () => {});
|
|
57
|
+
const manager = new TaskManager(
|
|
58
|
+
provider({
|
|
59
|
+
updateTask,
|
|
60
|
+
getCompletionBlockers: vi.fn(async () => [{ texto: 'Documentar', linha: 12 }]),
|
|
61
|
+
} as Partial<ITaskProvider>),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const { task } = await manager.finishTaskComRelato(parseTaskId('001'));
|
|
65
|
+
|
|
66
|
+
expect(task.status).toBe('done');
|
|
67
|
+
expect(updateTask).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('nao relata nada quando tudo esta feito ou adiado', async () => {
|
|
71
|
+
const manager = new TaskManager(
|
|
72
|
+
provider({ getCompletionBlockers: vi.fn(async () => []) } as Partial<ITaskProvider>),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect((await manager.finishTaskComRelato(parseTaskId('001'))).blockers).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
type CriterionSurface,
|
|
5
|
+
FILTER_CRITERIA_SURFACES,
|
|
6
|
+
FilterCriteriaSchema,
|
|
7
|
+
filterCriteriaCliOptions,
|
|
8
|
+
filterCriteriaJsonSchema,
|
|
9
|
+
parseFilterCriteria,
|
|
10
|
+
type TaskFilterCriteria,
|
|
11
|
+
} from './filter-criteria.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Um criterio sem superficie e **erro de compilacao**, e nao aviso em tempo de
|
|
15
|
+
* execucao. Esta linha e a prova: um mapa que cobre `TaskFilterCriteria` menos a
|
|
16
|
+
* chave `text` nao satisfaz o gate por tipo. O `@ts-expect-error` falha o
|
|
17
|
+
* `typecheck` se um dia a omissao passar a compilar — que e o defeito que a task
|
|
18
|
+
* fecha.
|
|
19
|
+
*/
|
|
20
|
+
const semText = { status: FILTER_CRITERIA_SURFACES.status };
|
|
21
|
+
// @ts-expect-error — falta a chave 'text': um criterio sem superficie nao compila.
|
|
22
|
+
const _gateIncompleto: Record<keyof TaskFilterCriteria, CriterionSurface> = semText;
|
|
23
|
+
void _gateIncompleto;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Uma definicao so para os criterios de filtro.
|
|
27
|
+
*
|
|
28
|
+
* A propriedade que vale nao afirma a forma do schema — afirma que um criterio
|
|
29
|
+
* definido num lugar so **aparece nas duas superficies** sem edicao manual. Por
|
|
30
|
+
* isso os geradores recebem um schema e superficies: o teste acrescenta um
|
|
31
|
+
* criterio ficticio e prova que ele emerge na CLI e no MCP, o que so e possivel
|
|
32
|
+
* se as superficies derivarem, em vez de repetir a lista a mao.
|
|
33
|
+
*/
|
|
34
|
+
describe('criterios de filtro derivam de uma definicao so', () => {
|
|
35
|
+
const schemaEstendido = FilterCriteriaSchema.extend({ fictional: z.string().optional() });
|
|
36
|
+
const superficiesEstendidas = {
|
|
37
|
+
...FILTER_CRITERIA_SURFACES,
|
|
38
|
+
fictional: { description: 'criterio ficticio para o teste', cli: { kind: 'flag' as const } },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
it('um criterio novo aparece no schema JSON do MCP sem editar o MCP', () => {
|
|
42
|
+
const json = filterCriteriaJsonSchema(schemaEstendido, superficiesEstendidas);
|
|
43
|
+
|
|
44
|
+
expect(json.properties).toHaveProperty('fictional');
|
|
45
|
+
expect((json.properties.fictional as { description?: string }).description).toBe('criterio ficticio para o teste');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('um criterio novo aparece nas opcoes da CLI sem editar a CLI', () => {
|
|
49
|
+
const opcoes = filterCriteriaCliOptions(schemaEstendido, superficiesEstendidas);
|
|
50
|
+
|
|
51
|
+
const ficticia = opcoes.find((o) => o.flags.includes('--fictional'));
|
|
52
|
+
expect(ficticia).toBeDefined();
|
|
53
|
+
expect(ficticia?.description).toBe('criterio ficticio para o teste');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('cada criterio do schema tem uma superficie — nenhum fica para tras', () => {
|
|
57
|
+
const doSchema = Object.keys(FilterCriteriaSchema.shape).sort();
|
|
58
|
+
const comSuperficie = Object.keys(FILTER_CRITERIA_SURFACES).sort();
|
|
59
|
+
|
|
60
|
+
expect(comSuperficie).toEqual(doSchema);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('o schema JSON derivado do MCP', () => {
|
|
65
|
+
it('descreve todo criterio, e usa enum onde o valor e fechado', () => {
|
|
66
|
+
const json = filterCriteriaJsonSchema();
|
|
67
|
+
|
|
68
|
+
expect(json.type).toBe('object');
|
|
69
|
+
expect(json.required).toEqual([]);
|
|
70
|
+
// status e um conjunto fechado: o schema JSON traz o enum, e nao string solta.
|
|
71
|
+
expect((json.properties.status as { enum?: string[] }).enum).toContain('pending');
|
|
72
|
+
expect((json.properties.open as { type?: string }).type).toBe('boolean');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('as opcoes derivadas da CLI', () => {
|
|
77
|
+
it('booleano nao leva valor; os demais levam <chave>', () => {
|
|
78
|
+
const opcoes = filterCriteriaCliOptions();
|
|
79
|
+
const porFlag = (frag: string) => opcoes.find((o) => o.flags.includes(frag));
|
|
80
|
+
|
|
81
|
+
expect(porFlag('--open')?.flags).toBe('--open');
|
|
82
|
+
expect(porFlag('--status')?.flags).toBe('-s, --status <status>');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('unifica a grafia: a flag do responsavel e --assignee, e nao --user', () => {
|
|
86
|
+
const opcoes = filterCriteriaCliOptions();
|
|
87
|
+
|
|
88
|
+
expect(opcoes.some((o) => o.flags.includes('--assignee'))).toBe(true);
|
|
89
|
+
expect(opcoes.some((o) => o.flags.includes('--user'))).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('o texto livre e argumento posicional, e nao vira flag', () => {
|
|
93
|
+
const opcoes = filterCriteriaCliOptions();
|
|
94
|
+
|
|
95
|
+
expect(opcoes.some((o) => o.flags.includes('--text'))).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('parseFilterCriteria', () => {
|
|
100
|
+
it('descarta chaves que nao sao criterio, como o --json', () => {
|
|
101
|
+
const criteria = parseFilterCriteria({ status: 'done', json: true, foo: 1 });
|
|
102
|
+
|
|
103
|
+
expect(criteria).toEqual({ status: 'done' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('falha barulhento num valor fora do conjunto, em vez de silenciar', () => {
|
|
107
|
+
expect(() => parseFilterCriteria({ status: 'inexistente' })).toThrow();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('objeto vazio e um criterio valido que nao restringe nada', () => {
|
|
111
|
+
expect(parseFilterCriteria({})).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { TaskStatusSchema, TaskTypeSchema } from '@opentask/taskin-types';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Os criterios que restringem uma listagem, definidos **uma vez so**.
|
|
6
|
+
*
|
|
7
|
+
* Antes cada criterio existia em cinco lugares escritos a mao: o tipo, a flag da
|
|
8
|
+
* CLI, o mapeamento da CLI, o schema JSON do MCP e o mapeamento do MCP.
|
|
9
|
+
* Acrescentar um exigia lembrar dos cinco, e esquecer nao quebrava nada — so
|
|
10
|
+
* fazia uma superficie ficar para tras, em silencio. O sintoma ja tinha
|
|
11
|
+
* aparecido: o criterio chamava-se `assignee` e a flag da CLI chamava-se
|
|
12
|
+
* `--user`, duas grafias para a mesma coisa que ninguem decidiu.
|
|
13
|
+
*
|
|
14
|
+
* Aqui o schema e a fonte. As superficies **derivam** dele:
|
|
15
|
+
*
|
|
16
|
+
* - `TaskFilterCriteria` e `z.infer` deste schema — o tipo nao se escreve a mao.
|
|
17
|
+
* - O schema JSON do `list_tasks` sai de `filterCriteriaJsonSchema`.
|
|
18
|
+
* - As opcoes do `taskin list` saem de `filterCriteriaCliOptions`.
|
|
19
|
+
* - A validacao e a conversao saem de `parseFilterCriteria` — os dois
|
|
20
|
+
* mapeamentos a mao desaparecem.
|
|
21
|
+
*
|
|
22
|
+
* O portao que fecha o circuito e o `satisfies Record<keyof TaskFilterCriteria,
|
|
23
|
+
* ...>` em {@link FILTER_CRITERIA_SURFACES}: acrescentar um criterio ao schema e
|
|
24
|
+
* **nao** ligar a superficie vira erro de compilacao, que e o unico lembrete que
|
|
25
|
+
* nao depende de ninguem lembrar.
|
|
26
|
+
*
|
|
27
|
+
* **Como acrescentar um criterio novo.** Duas linhas, e o compilador cobra a
|
|
28
|
+
* segunda:
|
|
29
|
+
*
|
|
30
|
+
* 1. uma propriedade `.optional()` em {@link FilterCriteriaSchema};
|
|
31
|
+
* 2. a entrada correspondente em {@link FILTER_CRITERIA_SURFACES} (descricao +
|
|
32
|
+
* onde mora na CLI).
|
|
33
|
+
*
|
|
34
|
+
* A CLI, o schema JSON do MCP e a validacao passam a conhece-lo sem mais nenhuma
|
|
35
|
+
* edicao. Esquecer o passo 2 nao compila.
|
|
36
|
+
*/
|
|
37
|
+
export const FilterCriteriaSchema = z.object({
|
|
38
|
+
status: TaskStatusSchema.optional(),
|
|
39
|
+
type: TaskTypeSchema.optional(),
|
|
40
|
+
assignee: z.string().optional(),
|
|
41
|
+
open: z.boolean().optional(),
|
|
42
|
+
closed: z.boolean().optional(),
|
|
43
|
+
active: z.boolean().optional(),
|
|
44
|
+
text: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* O que restringe uma listagem de tarefas.
|
|
49
|
+
*
|
|
50
|
+
* Todos os campos sao opcionais e **se somam**: informar dois exige os dois. Um
|
|
51
|
+
* criterio ausente nao restringe nada. Derivado de {@link FilterCriteriaSchema}
|
|
52
|
+
* — mudar o schema muda o tipo.
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export type TaskFilterCriteria = z.infer<typeof FilterCriteriaSchema>;
|
|
57
|
+
|
|
58
|
+
/** Onde um criterio aparece na linha de comando. */
|
|
59
|
+
export type CriterionCli = { readonly kind: 'positional' } | { readonly kind: 'flag'; readonly short?: string };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* O que uma superficie precisa saber de um criterio alem da sua forma: o texto
|
|
63
|
+
* de ajuda (compartilhado entre `--help` da CLI e a propriedade do MCP) e onde
|
|
64
|
+
* ele mora na linha de comando.
|
|
65
|
+
*/
|
|
66
|
+
export interface CriterionSurface {
|
|
67
|
+
readonly description: string;
|
|
68
|
+
readonly cli: CriterionCli;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A superficie de cada criterio, gateada a cobrir **todos** os campos de
|
|
73
|
+
* {@link TaskFilterCriteria}.
|
|
74
|
+
*
|
|
75
|
+
* O `satisfies Record<keyof TaskFilterCriteria, ...>` e o portao: acrescentar um
|
|
76
|
+
* criterio ao schema sem uma entrada aqui nao compila.
|
|
77
|
+
*/
|
|
78
|
+
export const FILTER_CRITERIA_SURFACES = {
|
|
79
|
+
status: { description: 'Exact status (pending, in-progress, done, ...)', cli: { kind: 'flag', short: 's' } },
|
|
80
|
+
type: { description: 'Exact type (feat, fix, chore, ...)', cli: { kind: 'flag', short: 't' } },
|
|
81
|
+
assignee: { description: 'Assignee id or name, whole or in part', cli: { kind: 'flag', short: 'u' } },
|
|
82
|
+
open: { description: 'Only tasks still open', cli: { kind: 'flag' } },
|
|
83
|
+
closed: { description: 'Only tasks already closed', cli: { kind: 'flag' } },
|
|
84
|
+
active: {
|
|
85
|
+
description: 'Only tasks started and not finished (in-progress, paused, in-review)',
|
|
86
|
+
cli: { kind: 'flag' },
|
|
87
|
+
},
|
|
88
|
+
text: { description: 'Free text over id, title, status and assignee', cli: { kind: 'positional' } },
|
|
89
|
+
} satisfies Record<keyof TaskFilterCriteria, CriterionSurface>;
|
|
90
|
+
|
|
91
|
+
type SurfaceMap = Record<string, CriterionSurface>;
|
|
92
|
+
|
|
93
|
+
/** O schema JSON de entrada de uma ferramenta MCP: objeto com propriedades. */
|
|
94
|
+
export interface FilterJsonSchema {
|
|
95
|
+
readonly type: 'object';
|
|
96
|
+
readonly properties: Record<string, unknown>;
|
|
97
|
+
readonly required: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Uma opcao de CLI, na forma que o `commander` (e o `defineCommand`) consome. */
|
|
101
|
+
export interface FilterCliOption {
|
|
102
|
+
readonly flags: string;
|
|
103
|
+
readonly description: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* O schema JSON das propriedades de filtro, derivado do zod.
|
|
108
|
+
*
|
|
109
|
+
* Nao e aposta: o `packages/types-ts` ja gera JSON Schema de schemas zod pelo
|
|
110
|
+
* mesmo `z.toJSONSchema()` desde a migracao para o zod 4. Aqui e o mesmo
|
|
111
|
+
* mecanismo, aplicado ao schema de filtro, com a descricao de cada campo vinda
|
|
112
|
+
* da superficie unica.
|
|
113
|
+
*
|
|
114
|
+
* Recebe schema e superficies para poder ser exercitado com um criterio
|
|
115
|
+
* ficticio nos testes — a prova de que deriva, em vez de repetir a mao.
|
|
116
|
+
*/
|
|
117
|
+
export function filterCriteriaJsonSchema(
|
|
118
|
+
schema: z.ZodObject = FilterCriteriaSchema,
|
|
119
|
+
surfaces: SurfaceMap = FILTER_CRITERIA_SURFACES,
|
|
120
|
+
): FilterJsonSchema {
|
|
121
|
+
const described = z.object(
|
|
122
|
+
Object.fromEntries(
|
|
123
|
+
Object.entries(schema.shape).map(([key, prop]) => [
|
|
124
|
+
key,
|
|
125
|
+
(prop as z.ZodType).describe(surfaces[key]?.description ?? ''),
|
|
126
|
+
]),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const json = z.toJSONSchema(described, { target: 'draft-7', io: 'output' }) as {
|
|
131
|
+
properties?: Record<string, unknown>;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return { type: 'object', properties: json.properties ?? {}, required: [] };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* As opcoes do `taskin list`, derivadas das superficies.
|
|
139
|
+
*
|
|
140
|
+
* Um criterio `positional` (o texto livre) nao vira flag: ele e o argumento
|
|
141
|
+
* `[filter]` do comando. Um criterio booleano nao leva valor (`--open`); os
|
|
142
|
+
* demais levam (`-s, --status <status>`). O tipo — booleano ou nao — sai do
|
|
143
|
+
* proprio schema JSON, e nao de uma segunda lista.
|
|
144
|
+
*/
|
|
145
|
+
export function filterCriteriaCliOptions(
|
|
146
|
+
schema: z.ZodObject = FilterCriteriaSchema,
|
|
147
|
+
surfaces: SurfaceMap = FILTER_CRITERIA_SURFACES,
|
|
148
|
+
): FilterCliOption[] {
|
|
149
|
+
const { properties } = filterCriteriaJsonSchema(schema, surfaces);
|
|
150
|
+
const options: FilterCliOption[] = [];
|
|
151
|
+
|
|
152
|
+
for (const [key, surface] of Object.entries(surfaces)) {
|
|
153
|
+
if (surface.cli.kind !== 'flag') continue;
|
|
154
|
+
|
|
155
|
+
const short = surface.cli.short ? `-${surface.cli.short}, ` : '';
|
|
156
|
+
const takesValue = (properties[key] as { type?: string } | undefined)?.type !== 'boolean';
|
|
157
|
+
const flags = takesValue ? `${short}--${key} <${key}>` : `${short}--${key}`;
|
|
158
|
+
|
|
159
|
+
options.push({ flags, description: surface.description });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return options;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Valida os argumentos crus e os converte em {@link TaskFilterCriteria}.
|
|
167
|
+
*
|
|
168
|
+
* E o `parse` do proprio schema, sem mapeamento a mao. Chaves desconhecidas sao
|
|
169
|
+
* descartadas (o `--json` do CLI, por exemplo, nao e criterio); um valor com o
|
|
170
|
+
* tipo errado — um status fora do conjunto — passa a **falhar barulhento**, em
|
|
171
|
+
* vez de devolver lista vazia como se nao houvesse tarefa.
|
|
172
|
+
*/
|
|
173
|
+
export function parseFilterCriteria(raw: unknown): TaskFilterCriteria {
|
|
174
|
+
return FilterCriteriaSchema.parse(raw);
|
|
175
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parseTaskId, type Task } from '@opentask/taskin-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { filterTasks } from './filter-tasks.js';
|
|
4
|
+
|
|
5
|
+
function tarefa({ id, ...resto }: Omit<Partial<Task>, 'id'> & { id: string }): Task {
|
|
6
|
+
return {
|
|
7
|
+
createdAt: '2026-09-12T00:00:00.000Z',
|
|
8
|
+
status: 'pending',
|
|
9
|
+
title: `Tarefa ${id}`,
|
|
10
|
+
type: 'feat',
|
|
11
|
+
...resto,
|
|
12
|
+
id: parseTaskId(id),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ANA = { id: 'ana-souza', name: 'Ana Souza', email: 'ana@example.com' };
|
|
17
|
+
const JOAO = { id: 'joao', name: 'João Silva', email: 'joao@example.com' };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A selecao de tarefas, isolada de como ela e exibida.
|
|
21
|
+
*
|
|
22
|
+
* Existiam duas implementacoes divergentes: o comando `list` casava assignee
|
|
23
|
+
* por substring em nome **ou** id, e o `Taskin.list` casava `userId` exato — e
|
|
24
|
+
* ainda projetava a task derrubando o proprio `assignee`. Duas respostas
|
|
25
|
+
* diferentes para a mesma pergunta, e o servidor MCP sem nenhuma.
|
|
26
|
+
*/
|
|
27
|
+
describe('filterTasks', () => {
|
|
28
|
+
const TAREFAS: readonly Task[] = [
|
|
29
|
+
tarefa({ id: '001', status: 'pending', type: 'feat', assignee: ANA, title: 'Criar login' }),
|
|
30
|
+
tarefa({ id: '002', status: 'done', type: 'fix', assignee: JOAO, title: 'Corrigir crash' }),
|
|
31
|
+
tarefa({ id: '003', status: 'in-progress', type: 'feat', assignee: ANA, title: 'Painel de metricas' }),
|
|
32
|
+
tarefa({ id: '004', status: 'blocked', type: 'chore', title: 'Sem responsavel' }),
|
|
33
|
+
tarefa({ id: '005', status: 'canceled', type: 'docs', assignee: JOAO, title: 'Guia antigo' }),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
it('sem criterio, devolve tudo na ordem recebida', () => {
|
|
37
|
+
expect(filterTasks(TAREFAS, {}).map((t) => t.id)).toEqual(['001', '002', '003', '004', '005']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('filtra por status', () => {
|
|
41
|
+
expect(filterTasks(TAREFAS, { status: 'done' }).map((t) => t.id)).toEqual(['002']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('filtra por tipo', () => {
|
|
45
|
+
expect(filterTasks(TAREFAS, { type: 'feat' }).map((t) => t.id)).toEqual(['001', '003']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('`open` traz o que ainda esta em aberto, e nao o que foi encerrado', () => {
|
|
49
|
+
expect(filterTasks(TAREFAS, { open: true }).map((t) => t.id)).toEqual(['001', '003', '004']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('`closed` traz done e canceled', () => {
|
|
53
|
+
expect(filterTasks(TAREFAS, { closed: true }).map((t) => t.id)).toEqual(['002', '005']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('casa o assignee pelo id, pelo nome e por parte dele', () => {
|
|
57
|
+
expect(filterTasks(TAREFAS, { assignee: 'ana-souza' }).map((t) => t.id)).toEqual(['001', '003']);
|
|
58
|
+
expect(filterTasks(TAREFAS, { assignee: 'Ana Souza' }).map((t) => t.id)).toEqual(['001', '003']);
|
|
59
|
+
expect(filterTasks(TAREFAS, { assignee: 'ana' }).map((t) => t.id)).toEqual(['001', '003']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('nao devolve tarefa sem responsavel quando se filtra por um', () => {
|
|
63
|
+
expect(filterTasks(TAREFAS, { assignee: 'ana' }).map((t) => t.id)).not.toContain('004');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('o texto livre procura em id, titulo, status e responsavel', () => {
|
|
67
|
+
expect(filterTasks(TAREFAS, { text: 'crash' }).map((t) => t.id)).toEqual(['002']);
|
|
68
|
+
expect(filterTasks(TAREFAS, { text: '003' }).map((t) => t.id)).toEqual(['003']);
|
|
69
|
+
expect(filterTasks(TAREFAS, { text: 'blocked' }).map((t) => t.id)).toEqual(['004']);
|
|
70
|
+
expect(filterTasks(TAREFAS, { text: 'joão' }).map((t) => t.id)).toEqual(['002', '005']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('combina criterios, exigindo todos', () => {
|
|
74
|
+
expect(filterTasks(TAREFAS, { type: 'feat', assignee: 'ana', status: 'pending' }).map((t) => t.id)).toEqual([
|
|
75
|
+
'001',
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('nao altera o arranjo recebido', () => {
|
|
80
|
+
const original = [...TAREFAS];
|
|
81
|
+
filterTasks(TAREFAS, { status: 'done' });
|
|
82
|
+
expect(TAREFAS).toEqual(original);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* "Em andamento" nao e `in-progress`, e nao e `open`.
|
|
88
|
+
*
|
|
89
|
+
* `open` inclui `pending` — o que ainda nao comecou, e que no painel e ruido.
|
|
90
|
+
* `in-progress` sozinho e estreito demais: uma tarefa some da tela no instante
|
|
91
|
+
* em que alguem a pausa ou a manda para revisao, o que e enganoso.
|
|
92
|
+
*
|
|
93
|
+
* `active` e o meio que faltava: **comecou e nao terminou**. `blocked` fica de
|
|
94
|
+
* fora por decisao declarada — ninguem esta trabalhando numa bloqueada agora.
|
|
95
|
+
*/
|
|
96
|
+
describe('filtro active', () => {
|
|
97
|
+
const tarefas = [
|
|
98
|
+
tarefa({ id: '001', status: 'pending' }),
|
|
99
|
+
tarefa({ id: '002', status: 'in-progress' }),
|
|
100
|
+
tarefa({ id: '003', status: 'paused' }),
|
|
101
|
+
tarefa({ id: '004', status: 'in-review' }),
|
|
102
|
+
tarefa({ id: '005', status: 'blocked' }),
|
|
103
|
+
tarefa({ id: '006', status: 'done' }),
|
|
104
|
+
tarefa({ id: '007', status: 'canceled' }),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
it('traz o que comecou e nao terminou', () => {
|
|
108
|
+
const ids = filterTasks(tarefas, { active: true }).map((t) => String(t.id));
|
|
109
|
+
|
|
110
|
+
expect(ids).toEqual(['002', '003', '004']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('deixa pending de fora — nao comecou', () => {
|
|
114
|
+
expect(filterTasks(tarefas, { active: true }).map((t) => String(t.id))).not.toContain('001');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('deixa blocked de fora, por decisao declarada', () => {
|
|
118
|
+
expect(filterTasks(tarefas, { active: true }).map((t) => String(t.id))).not.toContain('005');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('e mais estreito que open e mais largo que in-progress', () => {
|
|
122
|
+
const abertas = filterTasks(tarefas, { open: true }).length;
|
|
123
|
+
const ativas = filterTasks(tarefas, { active: true }).length;
|
|
124
|
+
const emProgresso = filterTasks(tarefas, { status: 'in-progress' }).length;
|
|
125
|
+
|
|
126
|
+
expect(ativas).toBeLessThan(abertas);
|
|
127
|
+
expect(ativas).toBeGreaterThan(emProgresso);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Task, TaskStatus } from '@opentask/taskin-types';
|
|
2
|
+
import type { TaskFilterCriteria, TaskSummary } from './filter-tasks.types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* O que conta como "ainda em aberto".
|
|
6
|
+
*
|
|
7
|
+
* `paused` e `in-review` entram: a tarefa nao foi entregue nem descartada, so
|
|
8
|
+
* nao esta sendo tocada agora. Deixa-las de fora escondia trabalho em curso de
|
|
9
|
+
* quem filtrava por `--open`.
|
|
10
|
+
*/
|
|
11
|
+
const EM_ABERTO: readonly TaskStatus[] = ['pending', 'in-progress', 'paused', 'in-review', 'blocked'];
|
|
12
|
+
|
|
13
|
+
const ENCERRADOS: readonly TaskStatus[] = ['done', 'canceled'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Comecou e nao terminou.
|
|
17
|
+
*
|
|
18
|
+
* O meio que faltava entre `open` e `status: 'in-progress'`. `open` inclui
|
|
19
|
+
* `pending` — o que ainda nao comecou, e que num painel de acompanhamento e
|
|
20
|
+
* ruido. `in-progress` sozinho e estreito demais: a tarefa some da tela no
|
|
21
|
+
* instante em que alguem a pausa ou a manda para revisao.
|
|
22
|
+
*
|
|
23
|
+
* `blocked` fica de fora por decisao declarada: bloqueada e trabalho comecado,
|
|
24
|
+
* mas ninguem esta mexendo nela agora, e quem olha "o que esta andando" nao
|
|
25
|
+
* quer ve-la ali.
|
|
26
|
+
*/
|
|
27
|
+
const ATIVAS: readonly TaskStatus[] = ['in-progress', 'paused', 'in-review'];
|
|
28
|
+
|
|
29
|
+
const contem = (valor: string | undefined, procurado: string): boolean =>
|
|
30
|
+
valor?.toLowerCase().includes(procurado) ?? false;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Casa o responsavel por id ou nome, inteiro ou em parte.
|
|
34
|
+
*
|
|
35
|
+
* Tarefa sem responsavel nunca casa: filtrar por uma pessoa e perguntar "o que
|
|
36
|
+
* e dela", e "de ninguem" nao e resposta.
|
|
37
|
+
*/
|
|
38
|
+
function casaResponsavel(task: Task, procurado: string): boolean {
|
|
39
|
+
const alvo = procurado.toLowerCase();
|
|
40
|
+
return contem(task.assignee?.id, alvo) || contem(task.assignee?.name, alvo);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Seleciona tarefas por criterio, sem saber como elas serao exibidas.
|
|
45
|
+
*
|
|
46
|
+
* Vive aqui, no pacote agnostico, porque a mesma pergunta e feita de tres
|
|
47
|
+
* lugares: o comando `list`, a saida em JSON e o servidor MCP. Antes eram duas
|
|
48
|
+
* implementacoes que ja discordavam — o comando casava o responsavel por
|
|
49
|
+
* substring em nome ou id, e a classe `Taskin` casava `userId` exato.
|
|
50
|
+
*
|
|
51
|
+
* Nao ordena e nao muda o arranjo recebido: a ordem que entra e a que sai.
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export function filterTasks(tasks: readonly Task[], criteria: TaskFilterCriteria): Task[] {
|
|
56
|
+
return tasks.filter((task) => {
|
|
57
|
+
if (criteria.status !== undefined) {
|
|
58
|
+
if (task.status !== criteria.status) return false;
|
|
59
|
+
} else if (criteria.open && !EM_ABERTO.includes(task.status)) {
|
|
60
|
+
return false;
|
|
61
|
+
} else if (criteria.closed && !ENCERRADOS.includes(task.status)) {
|
|
62
|
+
return false;
|
|
63
|
+
} else if (criteria.active && !ATIVAS.includes(task.status)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (criteria.type !== undefined && task.type !== criteria.type) return false;
|
|
68
|
+
|
|
69
|
+
if (criteria.assignee !== undefined && !casaResponsavel(task, criteria.assignee)) return false;
|
|
70
|
+
|
|
71
|
+
if (criteria.text !== undefined) {
|
|
72
|
+
const procurado = criteria.text.toLowerCase();
|
|
73
|
+
const casa =
|
|
74
|
+
task.id.toLowerCase().includes(procurado) ||
|
|
75
|
+
contem(task.title, procurado) ||
|
|
76
|
+
contem(task.status, procurado) ||
|
|
77
|
+
casaResponsavel(task, procurado);
|
|
78
|
+
if (!casa) return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* O que identifica uma tarefa numa listagem, sem o que ela diz.
|
|
87
|
+
*
|
|
88
|
+
* O provider de arquivos carrega o markdown inteiro em `content` e
|
|
89
|
+
* `description`. Incluir isso faria a listagem deste repositorio passar de
|
|
90
|
+
* vinte mil linhas — e quem lista quer **escolher** uma tarefa, nao le-las
|
|
91
|
+
* todas. O corpo se busca depois, pelo id.
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
export function summarizeTask(task: Task): TaskSummary {
|
|
96
|
+
return {
|
|
97
|
+
id: task.id,
|
|
98
|
+
title: task.title,
|
|
99
|
+
status: task.status,
|
|
100
|
+
type: task.type,
|
|
101
|
+
...(task.assignee && { assignee: { id: task.assignee.id, name: task.assignee.name } }),
|
|
102
|
+
...(task.order !== undefined && { priority: task.order }),
|
|
103
|
+
...(task.groupId && { groupId: task.groupId }),
|
|
104
|
+
...(task.difficulty !== undefined && { difficulty: task.difficulty }),
|
|
105
|
+
};
|
|
106
|
+
}
|