human-in-the-loop 0.1.0 → 2.0.2
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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +17 -0
- package/README.md +600 -166
- package/dist/helpers.d.ts +308 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +275 -0
- package/dist/helpers.js.map +1 -0
- package/dist/human.d.ts +315 -0
- package/dist/human.d.ts.map +1 -0
- package/dist/human.js +476 -0
- package/dist/human.js.map +1 -0
- package/dist/index.d.ts +48 -343
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -793
- package/dist/index.js.map +1 -0
- package/dist/store.d.ts +61 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +162 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +399 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/examples/basic-usage.ts +386 -0
- package/package.json +23 -54
- package/src/helpers.ts +379 -0
- package/src/human.test.ts +384 -0
- package/src/human.ts +626 -0
- package/src/index.ts +102 -6
- package/src/store.ts +201 -0
- package/src/types.ts +431 -0
- package/tsconfig.json +6 -11
- package/TODO.md +0 -53
- package/dist/index.cjs +0 -899
- package/dist/index.d.cts +0 -344
- package/src/core/factory.test.ts +0 -69
- package/src/core/factory.ts +0 -30
- package/src/core/types.ts +0 -191
- package/src/platforms/email/index.tsx +0 -137
- package/src/platforms/react/index.tsx +0 -218
- package/src/platforms/slack/index.ts +0 -84
- package/src/platforms/teams/index.ts +0 -84
- package/vitest.config.ts +0 -15
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for human-in-the-loop primitives
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
6
|
+
import { Human } from './human.js'
|
|
7
|
+
import { InMemoryHumanStore } from './store.js'
|
|
8
|
+
import type { ApprovalResponse, ReviewResponse } from './types.js'
|
|
9
|
+
|
|
10
|
+
describe('Human-in-the-loop', () => {
|
|
11
|
+
let human: ReturnType<typeof Human>
|
|
12
|
+
let store: InMemoryHumanStore
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
store = new InMemoryHumanStore()
|
|
16
|
+
human = Human({ store })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('Role Management', () => {
|
|
20
|
+
it('should define and retrieve a role', () => {
|
|
21
|
+
const role = human.defineRole({
|
|
22
|
+
id: 'tech-lead',
|
|
23
|
+
name: 'Tech Lead',
|
|
24
|
+
description: 'Technical leadership',
|
|
25
|
+
capabilities: ['approve-prs', 'deploy-prod'],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(role.id).toBe('tech-lead')
|
|
29
|
+
expect(role.name).toBe('Tech Lead')
|
|
30
|
+
|
|
31
|
+
const retrieved = human.getRole('tech-lead')
|
|
32
|
+
expect(retrieved).toEqual(role)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('Team Management', () => {
|
|
37
|
+
it('should define and retrieve a team', () => {
|
|
38
|
+
const team = human.defineTeam({
|
|
39
|
+
id: 'engineering',
|
|
40
|
+
name: 'Engineering Team',
|
|
41
|
+
members: ['alice', 'bob'],
|
|
42
|
+
lead: 'alice',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(team.id).toBe('engineering')
|
|
46
|
+
expect(team.members).toHaveLength(2)
|
|
47
|
+
|
|
48
|
+
const retrieved = human.getTeam('engineering')
|
|
49
|
+
expect(retrieved).toEqual(team)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('Human Worker Management', () => {
|
|
54
|
+
it('should register and retrieve a human', () => {
|
|
55
|
+
const worker = human.registerHuman({
|
|
56
|
+
id: 'alice',
|
|
57
|
+
name: 'Alice Smith',
|
|
58
|
+
email: 'alice@example.com',
|
|
59
|
+
roles: ['tech-lead'],
|
|
60
|
+
teams: ['engineering'],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(worker.id).toBe('alice')
|
|
64
|
+
expect(worker.name).toBe('Alice Smith')
|
|
65
|
+
|
|
66
|
+
const retrieved = human.getHuman('alice')
|
|
67
|
+
expect(retrieved).toEqual(worker)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('Approval Requests', () => {
|
|
72
|
+
it('should create an approval request', async () => {
|
|
73
|
+
const requestPromise = human.approve({
|
|
74
|
+
title: 'Test Approval',
|
|
75
|
+
description: 'Test approval request',
|
|
76
|
+
subject: 'Test',
|
|
77
|
+
input: { data: 'test' },
|
|
78
|
+
assignee: 'alice@example.com',
|
|
79
|
+
priority: 'normal',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// The request should be pending in the store
|
|
83
|
+
const requests = await store.list({ status: ['pending'] })
|
|
84
|
+
expect(requests).toHaveLength(1)
|
|
85
|
+
expect(requests[0]?.type).toBe('approval')
|
|
86
|
+
expect(requests[0]?.title).toBe('Test Approval')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should complete an approval request', async () => {
|
|
90
|
+
// Create request
|
|
91
|
+
const request = await store.create({
|
|
92
|
+
type: 'approval' as const,
|
|
93
|
+
status: 'pending' as const,
|
|
94
|
+
title: 'Test Approval',
|
|
95
|
+
description: 'Test',
|
|
96
|
+
subject: 'Test',
|
|
97
|
+
input: { data: 'test' },
|
|
98
|
+
priority: 'normal' as const,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Complete it
|
|
102
|
+
const response: ApprovalResponse = {
|
|
103
|
+
approved: true,
|
|
104
|
+
comments: 'Looks good!',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const completed = await human.completeRequest(request.id, response)
|
|
108
|
+
expect(completed.status).toBe('completed')
|
|
109
|
+
expect(completed.response).toEqual(response)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should reject an approval request', async () => {
|
|
113
|
+
const request = await store.create({
|
|
114
|
+
type: 'approval' as const,
|
|
115
|
+
status: 'pending' as const,
|
|
116
|
+
title: 'Test Approval',
|
|
117
|
+
description: 'Test',
|
|
118
|
+
subject: 'Test',
|
|
119
|
+
input: { data: 'test' },
|
|
120
|
+
priority: 'normal' as const,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const rejected = await human.rejectRequest(request.id, 'Not ready yet')
|
|
124
|
+
expect(rejected.status).toBe('rejected')
|
|
125
|
+
expect(rejected.rejectionReason).toBe('Not ready yet')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('Question Requests', () => {
|
|
130
|
+
it('should create a question request', async () => {
|
|
131
|
+
const requestPromise = human.ask({
|
|
132
|
+
title: 'Test Question',
|
|
133
|
+
question: 'What is the answer?',
|
|
134
|
+
context: { topic: 'testing' },
|
|
135
|
+
assignee: 'alice@example.com',
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const requests = await store.list({ status: ['pending'] })
|
|
139
|
+
expect(requests).toHaveLength(1)
|
|
140
|
+
expect(requests[0]?.type).toBe('question')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('Decision Requests', () => {
|
|
145
|
+
it('should create a decision request', async () => {
|
|
146
|
+
const requestPromise = human.decide({
|
|
147
|
+
title: 'Test Decision',
|
|
148
|
+
options: ['option1', 'option2', 'option3'],
|
|
149
|
+
context: { info: 'test' },
|
|
150
|
+
assignee: 'alice@example.com',
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const requests = await store.list({ status: ['pending'] })
|
|
154
|
+
expect(requests).toHaveLength(1)
|
|
155
|
+
expect(requests[0]?.type).toBe('decision')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('Review Requests', () => {
|
|
160
|
+
it('should create a review request', async () => {
|
|
161
|
+
const requestPromise = human.review({
|
|
162
|
+
title: 'Test Review',
|
|
163
|
+
content: { code: 'console.log("test")' },
|
|
164
|
+
reviewType: 'code',
|
|
165
|
+
criteria: ['syntax', 'style', 'logic'],
|
|
166
|
+
assignee: 'alice@example.com',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const requests = await store.list({ status: ['pending'] })
|
|
170
|
+
expect(requests).toHaveLength(1)
|
|
171
|
+
expect(requests[0]?.type).toBe('review')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should complete a review request', async () => {
|
|
175
|
+
const request = await store.create({
|
|
176
|
+
type: 'review' as const,
|
|
177
|
+
status: 'pending' as const,
|
|
178
|
+
title: 'Code Review',
|
|
179
|
+
description: 'Review PR',
|
|
180
|
+
input: { code: 'test' },
|
|
181
|
+
content: { code: 'test' },
|
|
182
|
+
priority: 'normal' as const,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const response: ReviewResponse = {
|
|
186
|
+
approved: true,
|
|
187
|
+
comments: 'Code looks good',
|
|
188
|
+
rating: 5,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const completed = await human.completeRequest(request.id, response)
|
|
192
|
+
expect(completed.status).toBe('completed')
|
|
193
|
+
expect(completed.response).toEqual(response)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('Notifications', () => {
|
|
198
|
+
it('should send a notification', async () => {
|
|
199
|
+
const notification = await human.notify({
|
|
200
|
+
type: 'info',
|
|
201
|
+
title: 'Test Notification',
|
|
202
|
+
message: 'This is a test',
|
|
203
|
+
recipient: 'alice@example.com',
|
|
204
|
+
channels: ['email'],
|
|
205
|
+
priority: 'normal',
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect(notification.id).toBeDefined()
|
|
209
|
+
expect(notification.type).toBe('info')
|
|
210
|
+
expect(notification.title).toBe('Test Notification')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Review Queue', () => {
|
|
215
|
+
it('should create and filter a review queue', async () => {
|
|
216
|
+
// Create multiple requests
|
|
217
|
+
await store.create({
|
|
218
|
+
type: 'approval' as const,
|
|
219
|
+
status: 'pending' as const,
|
|
220
|
+
title: 'High Priority',
|
|
221
|
+
description: 'Test',
|
|
222
|
+
subject: 'Test',
|
|
223
|
+
input: {},
|
|
224
|
+
priority: 'high' as const,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
await store.create({
|
|
228
|
+
type: 'approval' as const,
|
|
229
|
+
status: 'pending' as const,
|
|
230
|
+
title: 'Normal Priority',
|
|
231
|
+
description: 'Test',
|
|
232
|
+
subject: 'Test',
|
|
233
|
+
input: {},
|
|
234
|
+
priority: 'normal' as const,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await store.create({
|
|
238
|
+
type: 'approval' as const,
|
|
239
|
+
status: 'completed' as const,
|
|
240
|
+
title: 'Completed',
|
|
241
|
+
description: 'Test',
|
|
242
|
+
subject: 'Test',
|
|
243
|
+
input: {},
|
|
244
|
+
priority: 'normal' as const,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Get queue with filters
|
|
248
|
+
const queue = await human.getQueue({
|
|
249
|
+
name: 'High Priority Queue',
|
|
250
|
+
filters: {
|
|
251
|
+
status: ['pending'],
|
|
252
|
+
priority: ['high'],
|
|
253
|
+
},
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
expect(queue.items).toHaveLength(1)
|
|
257
|
+
expect(queue.items[0]?.priority).toBe('high')
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('Goals and OKRs', () => {
|
|
262
|
+
it('should define goals', () => {
|
|
263
|
+
const goals = human.defineGoals({
|
|
264
|
+
id: 'q1-2024',
|
|
265
|
+
objectives: ['Launch v2.0', 'Improve performance'],
|
|
266
|
+
targetDate: new Date('2024-03-31'),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
expect(goals.id).toBe('q1-2024')
|
|
270
|
+
expect(goals.objectives).toHaveLength(2)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should track KPIs', () => {
|
|
274
|
+
const kpi = human.trackKPIs({
|
|
275
|
+
id: 'response-time',
|
|
276
|
+
name: 'API Response Time',
|
|
277
|
+
value: 120,
|
|
278
|
+
target: 100,
|
|
279
|
+
unit: 'ms',
|
|
280
|
+
trend: 'down',
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
expect(kpi.id).toBe('response-time')
|
|
284
|
+
expect(kpi.value).toBe(120)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('should define OKRs', () => {
|
|
288
|
+
const okr = human.defineOKRs({
|
|
289
|
+
id: 'q1-okr',
|
|
290
|
+
objective: 'Improve performance',
|
|
291
|
+
keyResults: [
|
|
292
|
+
{
|
|
293
|
+
description: 'Reduce response time to <100ms',
|
|
294
|
+
progress: 0.75,
|
|
295
|
+
current: 120,
|
|
296
|
+
target: 100,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
period: 'Q1 2024',
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
expect(okr.id).toBe('q1-okr')
|
|
303
|
+
expect(okr.keyResults).toHaveLength(1)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('Workflow Management', () => {
|
|
308
|
+
it('should create and retrieve a workflow', () => {
|
|
309
|
+
const workflow = human.createWorkflow({
|
|
310
|
+
id: 'approval-workflow',
|
|
311
|
+
name: 'Approval Workflow',
|
|
312
|
+
steps: [
|
|
313
|
+
{
|
|
314
|
+
name: 'Step 1',
|
|
315
|
+
approvers: ['alice'],
|
|
316
|
+
requireAll: true,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'Step 2',
|
|
320
|
+
approvers: ['bob'],
|
|
321
|
+
requireAll: true,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
currentStep: 0,
|
|
325
|
+
status: 'pending',
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
expect(workflow.id).toBe('approval-workflow')
|
|
329
|
+
expect(workflow.steps).toHaveLength(2)
|
|
330
|
+
|
|
331
|
+
const retrieved = human.getWorkflow('approval-workflow')
|
|
332
|
+
expect(retrieved).toEqual(workflow)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('Request Operations', () => {
|
|
337
|
+
it('should get a request by ID', async () => {
|
|
338
|
+
const request = await store.create({
|
|
339
|
+
type: 'approval' as const,
|
|
340
|
+
status: 'pending' as const,
|
|
341
|
+
title: 'Test',
|
|
342
|
+
description: 'Test',
|
|
343
|
+
subject: 'Test',
|
|
344
|
+
input: {},
|
|
345
|
+
priority: 'normal' as const,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const retrieved = await human.getRequest(request.id)
|
|
349
|
+
expect(retrieved?.id).toBe(request.id)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should escalate a request', async () => {
|
|
353
|
+
const request = await store.create({
|
|
354
|
+
type: 'approval' as const,
|
|
355
|
+
status: 'pending' as const,
|
|
356
|
+
title: 'Test',
|
|
357
|
+
description: 'Test',
|
|
358
|
+
subject: 'Test',
|
|
359
|
+
input: {},
|
|
360
|
+
priority: 'normal' as const,
|
|
361
|
+
assignee: 'alice',
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const escalated = await human.escalateRequest(request.id, 'bob')
|
|
365
|
+
expect(escalated.status).toBe('escalated')
|
|
366
|
+
expect(escalated.assignee).toBe('bob')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should cancel a request', async () => {
|
|
370
|
+
const request = await store.create({
|
|
371
|
+
type: 'approval' as const,
|
|
372
|
+
status: 'pending' as const,
|
|
373
|
+
title: 'Test',
|
|
374
|
+
description: 'Test',
|
|
375
|
+
subject: 'Test',
|
|
376
|
+
input: {},
|
|
377
|
+
priority: 'normal' as const,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const cancelled = await human.cancelRequest(request.id)
|
|
381
|
+
expect(cancelled.status).toBe('cancelled')
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
})
|