hzl-core 2.0.0 → 2.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.
Files changed (58) hide show
  1. package/dist/__tests__/backup/backup-restore.test.js +2 -0
  2. package/dist/__tests__/backup/backup-restore.test.js.map +1 -1
  3. package/dist/__tests__/backup/import-export.test.js +2 -0
  4. package/dist/__tests__/backup/import-export.test.js.map +1 -1
  5. package/dist/__tests__/concurrency/stress.test.js +3 -1
  6. package/dist/__tests__/concurrency/stress.test.js.map +1 -1
  7. package/dist/__tests__/properties/invariants.test.js +126 -0
  8. package/dist/__tests__/properties/invariants.test.js.map +1 -1
  9. package/dist/db/__tests__/datastore.test.js +14 -0
  10. package/dist/db/__tests__/datastore.test.js.map +1 -1
  11. package/dist/db/migrations/index.d.ts.map +1 -1
  12. package/dist/db/migrations/index.js +44 -0
  13. package/dist/db/migrations/index.js.map +1 -1
  14. package/dist/db/schema.d.ts +1 -1
  15. package/dist/db/schema.d.ts.map +1 -1
  16. package/dist/db/schema.js +41 -0
  17. package/dist/db/schema.js.map +1 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.test.js +4 -0
  23. package/dist/index.test.js.map +1 -1
  24. package/dist/projections/rebuild.d.ts.map +1 -1
  25. package/dist/projections/rebuild.js +29 -17
  26. package/dist/projections/rebuild.js.map +1 -1
  27. package/dist/services/backup-service.d.ts.map +1 -1
  28. package/dist/services/backup-service.js +2 -0
  29. package/dist/services/backup-service.js.map +1 -1
  30. package/dist/services/hook-drain-service.d.ts +58 -0
  31. package/dist/services/hook-drain-service.d.ts.map +1 -0
  32. package/dist/services/hook-drain-service.js +388 -0
  33. package/dist/services/hook-drain-service.js.map +1 -0
  34. package/dist/services/hook-drain-service.test.d.ts +2 -0
  35. package/dist/services/hook-drain-service.test.d.ts.map +1 -0
  36. package/dist/services/hook-drain-service.test.js +167 -0
  37. package/dist/services/hook-drain-service.test.js.map +1 -0
  38. package/dist/services/search-service.d.ts +1 -0
  39. package/dist/services/search-service.d.ts.map +1 -1
  40. package/dist/services/search-service.js +12 -14
  41. package/dist/services/search-service.js.map +1 -1
  42. package/dist/services/search-service.test.js +14 -1
  43. package/dist/services/search-service.test.js.map +1 -1
  44. package/dist/services/task-service.d.ts +26 -4
  45. package/dist/services/task-service.d.ts.map +1 -1
  46. package/dist/services/task-service.js +97 -35
  47. package/dist/services/task-service.js.map +1 -1
  48. package/dist/services/task-service.test.js +87 -10
  49. package/dist/services/task-service.test.js.map +1 -1
  50. package/dist/services/workflow-service.d.ts +141 -0
  51. package/dist/services/workflow-service.d.ts.map +1 -0
  52. package/dist/services/workflow-service.js +664 -0
  53. package/dist/services/workflow-service.js.map +1 -0
  54. package/dist/services/workflow-service.test.d.ts +2 -0
  55. package/dist/services/workflow-service.test.d.ts.map +1 -0
  56. package/dist/services/workflow-service.test.js +213 -0
  57. package/dist/services/workflow-service.test.js.map +1 -0
  58. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  // packages/hzl-core/src/services/task-service.test.ts
2
2
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
- import { TaskService, CrossProjectDependencyError, AmbiguousPrefixError } from './task-service.js';
3
+ import { TaskService, AmbiguousPrefixError, InvalidDueMonthError, InvalidProgressError, InvalidStatusTransitionError, } from './task-service.js';
4
4
  import { ProjectService, ProjectNotFoundError } from './project-service.js';
5
5
  import { createTestDb } from '../db/test-utils.js';
6
6
  import { EventStore } from '../events/store.js';
@@ -413,6 +413,10 @@ describe('TaskService', () => {
413
413
  expect(commentEvent).toBeDefined();
414
414
  expect(commentEvent.data.text).toBe('Blocked on external dependency');
415
415
  });
416
+ it('throws InvalidStatusTransitionError when task is not in_progress', () => {
417
+ const task = taskService.createTask({ title: 'Test', project: 'inbox' });
418
+ expect(() => taskService.releaseTask(task.task_id)).toThrow(InvalidStatusTransitionError);
419
+ });
416
420
  });
417
421
  describe('archive', () => {
418
422
  it('transitions from any status to archived', () => {
@@ -829,6 +833,10 @@ describe('TaskService', () => {
829
833
  expect(() => taskService.listTasks({ dueMonth: '2026-13' }))
830
834
  .toThrow('Invalid month in dueMonth');
831
835
  });
836
+ it('throws InvalidDueMonthError for invalid dueMonth values', () => {
837
+ expect(() => taskService.listTasks({ dueMonth: 'bad-month' })).toThrow(InvalidDueMonthError);
838
+ expect(() => taskService.listTasks({ dueMonth: '2026-00' })).toThrow(InvalidDueMonthError);
839
+ });
832
840
  });
833
841
  describe('getBlockedByMap', () => {
834
842
  it('returns empty map when no tasks are blocked', () => {
@@ -1294,6 +1302,11 @@ describe('TaskService', () => {
1294
1302
  expect(() => taskService.setProgress(task.task_id, 50.5))
1295
1303
  .toThrow('Progress must be an integer between 0 and 100');
1296
1304
  });
1305
+ it('throws InvalidProgressError on invalid progress values', () => {
1306
+ const task = taskService.createTask({ title: 'Test', project: 'inbox' });
1307
+ expect(() => taskService.setProgress(task.task_id, -1)).toThrow(InvalidProgressError);
1308
+ expect(() => taskService.setProgress(task.task_id, 101)).toThrow(InvalidProgressError);
1309
+ });
1297
1310
  it('allows setting progress to 0 and 100', () => {
1298
1311
  const task = taskService.createTask({ title: 'Test', project: 'inbox' });
1299
1312
  taskService.setProgress(task.task_id, 0);
@@ -1529,16 +1542,30 @@ describe('TaskService', () => {
1529
1542
  // Both should potentially be eligible since both are terminal
1530
1543
  expect(eligible.length).toBeGreaterThanOrEqual(0); // May be 0 if not old enough
1531
1544
  });
1532
- });
1533
- describe('cross-project dependency validation', () => {
1534
- it('rejects dependency on task in different project', () => {
1535
- const taskInProjectA = taskService.createTask({ title: 'Task A', project: 'project-a' });
1536
- expect(() => taskService.createTask({
1537
- title: 'Task B',
1538
- project: 'project-b',
1539
- depends_on: [taskInProjectA.task_id],
1540
- })).toThrow(CrossProjectDependencyError);
1545
+ it('blocks pruning when dependent is active in another project', () => {
1546
+ projectService.createProject('prune-cross-a');
1547
+ projectService.createProject('prune-cross-b');
1548
+ const dependency = taskService.createTask({ title: 'Dependency', project: 'prune-cross-a' });
1549
+ const dependent = taskService.createTask({
1550
+ title: 'Dependent',
1551
+ project: 'prune-cross-b',
1552
+ depends_on: [dependency.task_id],
1553
+ });
1554
+ taskService.setStatus(dependency.task_id, TaskStatus.Ready);
1555
+ taskService.claimTask(dependency.task_id);
1556
+ taskService.completeTask(dependency.task_id);
1557
+ taskService.setStatus(dependent.task_id, TaskStatus.Ready);
1558
+ const future = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString();
1559
+ const eligible = taskService.previewPrunableTasks({
1560
+ project: 'prune-cross-a',
1561
+ olderThanDays: 1,
1562
+ asOf: future,
1563
+ });
1564
+ const depFound = eligible.find(t => t.task_id === dependency.task_id);
1565
+ expect(depFound).toBeUndefined();
1541
1566
  });
1567
+ });
1568
+ describe('cross-project dependency behavior', () => {
1542
1569
  it('allows dependency on task in same project', () => {
1543
1570
  const dependency = taskService.createTask({ title: 'Dep', project: 'project-a' });
1544
1571
  const dependent = taskService.createTask({
@@ -1557,6 +1584,15 @@ describe('TaskService', () => {
1557
1584
  });
1558
1585
  expect(task.task_id).toBeDefined();
1559
1586
  });
1587
+ it('allows cross-project dependency by default', () => {
1588
+ const taskInProjectA = taskService.createTask({ title: 'Task A', project: 'project-a' });
1589
+ const taskInProjectB = taskService.createTask({
1590
+ title: 'Task B',
1591
+ project: 'project-b',
1592
+ depends_on: [taskInProjectA.task_id],
1593
+ });
1594
+ expect(taskInProjectB.task_id).toBeDefined();
1595
+ });
1560
1596
  });
1561
1597
  describe('pruneEligible', () => {
1562
1598
  let taskServiceWithEventsDb;
@@ -1937,5 +1973,46 @@ describe('TaskService', () => {
1937
1973
  expect(resolved).toBeNull();
1938
1974
  });
1939
1975
  });
1976
+ describe('on_done hooks', () => {
1977
+ it('enqueues outbox item when task is created directly as done', () => {
1978
+ const hookTaskService = new TaskService(db, eventStore, projectionEngine, projectService, undefined, {
1979
+ onDone: { url: 'http://127.0.0.1:18789/events/inject' },
1980
+ });
1981
+ const task = hookTaskService.createTask({
1982
+ title: 'Done on create',
1983
+ project: 'inbox',
1984
+ initial_status: TaskStatus.Done,
1985
+ });
1986
+ const row = db
1987
+ .prepare('SELECT hook_name, status, url FROM hook_outbox WHERE payload LIKE ?')
1988
+ .get(`%"task_id":"${task.task_id}"%`);
1989
+ expect(row).toBeDefined();
1990
+ expect(row?.hook_name).toBe('on_done');
1991
+ expect(row?.status).toBe('queued');
1992
+ expect(row?.url).toBe('http://127.0.0.1:18789/events/inject');
1993
+ });
1994
+ it('enqueues outbox item when setStatus transitions to done', () => {
1995
+ const hookTaskService = new TaskService(db, eventStore, projectionEngine, projectService, undefined, {
1996
+ onDone: { url: 'http://127.0.0.1:18789/events/inject' },
1997
+ });
1998
+ const task = hookTaskService.createTask({ title: 'Set status done', project: 'inbox' });
1999
+ hookTaskService.setStatus(task.task_id, TaskStatus.Done);
2000
+ const count = db.prepare('SELECT COUNT(*) as count FROM hook_outbox WHERE payload LIKE ?')
2001
+ .get(`%"task_id":"${task.task_id}"%`).count;
2002
+ expect(count).toBe(1);
2003
+ });
2004
+ it('enqueues outbox item when completeTask transitions to done', () => {
2005
+ const hookTaskService = new TaskService(db, eventStore, projectionEngine, projectService, undefined, {
2006
+ onDone: { url: 'http://127.0.0.1:18789/events/inject' },
2007
+ });
2008
+ const task = hookTaskService.createTask({ title: 'Complete me', project: 'inbox' });
2009
+ hookTaskService.setStatus(task.task_id, TaskStatus.Ready);
2010
+ hookTaskService.claimTask(task.task_id, { author: 'agent-1' });
2011
+ hookTaskService.completeTask(task.task_id, { author: 'agent-1' });
2012
+ const count = db.prepare('SELECT COUNT(*) as count FROM hook_outbox WHERE payload LIKE ?')
2013
+ .get(`%"task_id":"${task.task_id}"%`).count;
2014
+ expect(count).toBe(1);
2015
+ });
2016
+ });
1940
2017
  });
1941
2018
  //# sourceMappingURL=task-service.test.js.map