@loopstack/github-oauth-example 0.1.1 → 0.2.1

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 (30) hide show
  1. package/README.md +116 -20
  2. package/dist/tools/authenticate-github-task.tool.d.ts +9 -8
  3. package/dist/tools/authenticate-github-task.tool.d.ts.map +1 -1
  4. package/dist/tools/authenticate-github-task.tool.js +30 -62
  5. package/dist/tools/authenticate-github-task.tool.js.map +1 -1
  6. package/dist/workflows/github-agent.ui.yaml +16 -0
  7. package/dist/workflows/github-agent.workflow.d.ts +14 -13
  8. package/dist/workflows/github-agent.workflow.d.ts.map +1 -1
  9. package/dist/workflows/github-agent.workflow.js +155 -71
  10. package/dist/workflows/github-agent.workflow.js.map +1 -1
  11. package/dist/workflows/github-repos-overview.ui.yaml +17 -0
  12. package/dist/workflows/github-repos-overview.workflow.d.ts +77 -87
  13. package/dist/workflows/github-repos-overview.workflow.d.ts.map +1 -1
  14. package/dist/workflows/github-repos-overview.workflow.js +177 -228
  15. package/dist/workflows/github-repos-overview.workflow.js.map +1 -1
  16. package/dist/workflows/templates/repoOverview.md +81 -0
  17. package/dist/workflows/templates/systemMessage.md +23 -0
  18. package/package.json +7 -7
  19. package/src/tools/authenticate-github-task.tool.ts +34 -82
  20. package/src/workflows/__tests__/github-repos-overview-workflow.spec.ts +105 -249
  21. package/src/workflows/github-agent.ui.yaml +16 -0
  22. package/src/workflows/github-agent.workflow.ts +109 -27
  23. package/src/workflows/github-repos-overview.ui.yaml +17 -0
  24. package/src/workflows/github-repos-overview.workflow.ts +257 -215
  25. package/src/workflows/templates/repoOverview.md +81 -0
  26. package/src/workflows/templates/systemMessage.md +23 -0
  27. package/dist/workflows/github-agent.workflow.yaml +0 -154
  28. package/dist/workflows/github-repos-overview.workflow.yaml +0 -249
  29. package/src/workflows/github-agent.workflow.yaml +0 -154
  30. package/src/workflows/github-repos-overview.workflow.yaml +0 -249
@@ -1,17 +1,7 @@
1
1
  import { TestingModule } from '@nestjs/testing';
2
2
  import { z } from 'zod';
3
- import {
4
- RunContext,
5
- generateObjectFingerprint,
6
- getBlockArgsSchema,
7
- getBlockConfig,
8
- getBlockDocuments,
9
- getBlockHelper,
10
- getBlockHelpers,
11
- getBlockStateSchema,
12
- getBlockTools,
13
- } from '@loopstack/common';
14
- import { CreateDocument, LoopCoreModule, Task, WorkflowProcessorService } from '@loopstack/core';
3
+ import { RunContext, WorkflowEntity, getBlockArgsSchema, getBlockConfig, getBlockTools } from '@loopstack/common';
4
+ import { LoopCoreModule, WorkflowProcessorService } from '@loopstack/core';
15
5
  import { CreateChatMessage, CreateChatMessageToolModule } from '@loopstack/create-chat-message-tool';
16
6
  import {
17
7
  GitHubCreateIssueCommentTool,
@@ -40,10 +30,14 @@ import {
40
30
  GitHubSearchReposTool,
41
31
  GitHubTriggerWorkflowTool,
42
32
  } from '@loopstack/github-module';
43
- import { OAuthModule } from '@loopstack/oauth-module';
44
- import { ToolMock, createWorkflowTest } from '@loopstack/testing';
33
+ import { OAuthModule, OAuthWorkflow } from '@loopstack/oauth-module';
34
+ import { ToolMock, createStatelessContext, createWorkflowTest } from '@loopstack/testing';
45
35
  import { GitHubReposOverviewWorkflow } from '../github-repos-overview.workflow';
46
36
 
37
+ const mockOAuthWorkflow = {
38
+ run: jest.fn(),
39
+ };
40
+
47
41
  function applyAllGitHubToolMocks(builder: ReturnType<typeof createWorkflowTest>) {
48
42
  return builder
49
43
  .withToolMock(GitHubGetAuthenticatedUserTool)
@@ -73,6 +67,16 @@ function applyAllGitHubToolMocks(builder: ReturnType<typeof createWorkflowTest>)
73
67
  .withToolMock(GitHubSearchIssuesTool);
74
68
  }
75
69
 
70
+ function buildWorkflowTest() {
71
+ return applyAllGitHubToolMocks(
72
+ createWorkflowTest()
73
+ .forWorkflow(GitHubReposOverviewWorkflow)
74
+ .withImports(LoopCoreModule, CreateChatMessageToolModule, OAuthModule)
75
+ .withMock(OAuthWorkflow, mockOAuthWorkflow)
76
+ .withToolOverride(CreateChatMessage),
77
+ );
78
+ }
79
+
76
80
  describe('GitHubReposOverviewWorkflow', () => {
77
81
  let module: TestingModule;
78
82
  let workflow: GitHubReposOverviewWorkflow;
@@ -87,21 +91,10 @@ describe('GitHubReposOverviewWorkflow', () => {
87
91
  let mockListDirectory: ToolMock;
88
92
  let mockListWorkflowRuns: ToolMock;
89
93
  let mockSearchCode: ToolMock;
90
- let mockTask: ToolMock;
91
- let mockCreateDocument: ToolMock;
92
-
93
- function buildWorkflowTest() {
94
- return applyAllGitHubToolMocks(
95
- createWorkflowTest()
96
- .forWorkflow(GitHubReposOverviewWorkflow)
97
- .withImports(LoopCoreModule, CreateChatMessageToolModule, OAuthModule)
98
- .withToolOverride(Task)
99
- .withToolOverride(CreateDocument)
100
- .withToolOverride(CreateChatMessage),
101
- );
102
- }
103
94
 
104
95
  beforeEach(async () => {
96
+ jest.clearAllMocks();
97
+
105
98
  module = await buildWorkflowTest().compile();
106
99
 
107
100
  workflow = module.get(GitHubReposOverviewWorkflow);
@@ -116,8 +109,6 @@ describe('GitHubReposOverviewWorkflow', () => {
116
109
  mockListDirectory = module.get(GitHubListDirectoryTool);
117
110
  mockListWorkflowRuns = module.get(GitHubListWorkflowRunsTool);
118
111
  mockSearchCode = module.get(GitHubSearchCodeTool);
119
- mockTask = module.get(Task);
120
- mockCreateDocument = module.get(CreateDocument);
121
112
  });
122
113
 
123
114
  afterEach(async () => {
@@ -136,71 +127,24 @@ describe('GitHubReposOverviewWorkflow', () => {
136
127
  expect(getBlockArgsSchema(workflow)).toBeInstanceOf(z.ZodType);
137
128
  });
138
129
 
139
- it('should have stateSchema defined', () => {
140
- expect(getBlockStateSchema(workflow)).toBeDefined();
141
- expect(getBlockStateSchema(workflow)).toBeInstanceOf(z.ZodType);
142
- });
143
-
144
130
  it('should have config defined', () => {
145
131
  expect(getBlockConfig(workflow)).toBeDefined();
146
132
  });
147
133
 
148
- it('should have all 28 tools available', () => {
134
+ it('should have GitHub tools available', () => {
149
135
  const tools = getBlockTools(workflow);
150
136
  expect(tools).toBeDefined();
151
- expect(tools).toHaveLength(28);
152
-
153
- // Core tools
154
- expect(tools).toContain('task');
155
- expect(tools).toContain('createDocument');
156
- expect(tools).toContain('createChatMessage');
157
137
 
158
- // GitHub — Users
159
138
  expect(tools).toContain('gitHubGetAuthenticatedUser');
160
139
  expect(tools).toContain('gitHubListUserOrgs');
161
-
162
- // GitHub — Repos
163
- expect(tools).toContain('gitHubListRepos');
164
140
  expect(tools).toContain('gitHubGetRepo');
165
- expect(tools).toContain('gitHubCreateRepo');
166
- expect(tools).toContain('gitHubListBranches');
167
141
 
168
- // GitHub — Issues
142
+ expect(tools).toContain('gitHubListBranches');
169
143
  expect(tools).toContain('gitHubListIssues');
170
- expect(tools).toContain('gitHubGetIssue');
171
- expect(tools).toContain('gitHubCreateIssue');
172
- expect(tools).toContain('gitHubCreateIssueComment');
173
-
174
- // GitHub — Pull Requests
175
144
  expect(tools).toContain('gitHubListPullRequests');
176
- expect(tools).toContain('gitHubGetPullRequest');
177
- expect(tools).toContain('gitHubCreatePullRequest');
178
- expect(tools).toContain('gitHubMergePullRequest');
179
- expect(tools).toContain('gitHubListPrReviews');
180
-
181
- // GitHub — Content
182
- expect(tools).toContain('gitHubGetFileContent');
183
- expect(tools).toContain('gitHubCreateOrUpdateFile');
184
145
  expect(tools).toContain('gitHubListDirectory');
185
- expect(tools).toContain('gitHubGetCommit');
186
-
187
- // GitHub — Actions
188
146
  expect(tools).toContain('gitHubListWorkflowRuns');
189
- expect(tools).toContain('gitHubTriggerWorkflow');
190
- expect(tools).toContain('gitHubGetWorkflowRun');
191
-
192
- // GitHub — Search
193
147
  expect(tools).toContain('gitHubSearchCode');
194
- expect(tools).toContain('gitHubSearchRepos');
195
- expect(tools).toContain('gitHubSearchIssues');
196
- });
197
-
198
- it('should have all documents available', () => {
199
- expect(getBlockDocuments(workflow)).toBeDefined();
200
- expect(Array.isArray(getBlockDocuments(workflow))).toBe(true);
201
- expect(getBlockDocuments(workflow)).toContain('linkDocument');
202
- expect(getBlockDocuments(workflow)).toContain('markdown');
203
- expect(getBlockDocuments(workflow)).toHaveLength(2);
204
148
  });
205
149
  });
206
150
 
@@ -235,61 +179,11 @@ describe('GitHubReposOverviewWorkflow', () => {
235
179
  });
236
180
  });
237
181
 
238
- describe('states', () => {
239
- it('should have stateSchema with expected properties', () => {
240
- const schema = getBlockStateSchema(workflow) as z.ZodObject<any>;
241
- expect(schema).toBeDefined();
242
- const shape = schema.shape;
243
- expect(shape.requiresAuthentication).toBeDefined();
244
- expect(shape.user).toBeDefined();
245
- expect(shape.orgs).toBeDefined();
246
- expect(shape.repo).toBeDefined();
247
- expect(shape.branches).toBeDefined();
248
- expect(shape.issues).toBeDefined();
249
- expect(shape.pullRequests).toBeDefined();
250
- expect(shape.directoryEntries).toBeDefined();
251
- expect(shape.workflowRuns).toBeDefined();
252
- expect(shape.searchResults).toBeDefined();
253
- });
254
-
255
- it('should validate state with all optional fields', () => {
256
- const schema = getBlockStateSchema(workflow)!;
257
- const result = schema.parse({});
258
- expect(result).toEqual({});
259
- });
260
-
261
- it('should throw error for invalid state field types', () => {
262
- const schema = getBlockStateSchema(workflow)!;
263
- expect(() => schema.parse({ repos: 'not-an-array' })).toThrow();
264
- expect(() => schema.parse({ requiresAuthentication: 'not-a-boolean' })).toThrow();
265
- });
266
- });
267
-
268
- describe('helpers', () => {
269
- it('should have helpers defined', () => {
270
- expect(getBlockHelpers(workflow)).toBeDefined();
271
- expect(Array.isArray(getBlockHelpers(workflow))).toBe(true);
272
- });
273
-
274
- it('should have searchQuery helper registered', () => {
275
- expect(getBlockHelpers(workflow)).toContain('searchQuery');
276
- });
277
-
278
- it('should execute searchQuery helper and return repo-scoped query', () => {
279
- // Set args on the workflow instance so the helper can read them
280
- (workflow as any).args = { owner: 'octocat', repo: 'Hello-World' };
281
- const helper = getBlockHelper(workflow, 'searchQuery')!;
282
- expect(helper).toBeDefined();
283
- const result = helper.call(workflow);
284
- expect(result).toBe('repo:octocat/Hello-World');
285
- });
286
- });
287
-
288
182
  describe('workflow execution — happy path', () => {
289
183
  const args = { owner: 'octocat', repo: 'Hello-World' };
290
184
 
291
185
  function setupAllMocks() {
292
- mockGetAuthenticatedUser.execute.mockResolvedValue({
186
+ mockGetAuthenticatedUser.call.mockResolvedValue({
293
187
  data: {
294
188
  user: {
295
189
  id: 1,
@@ -307,13 +201,13 @@ describe('GitHubReposOverviewWorkflow', () => {
307
201
  },
308
202
  });
309
203
 
310
- mockListUserOrgs.execute.mockResolvedValue({
204
+ mockListUserOrgs.call.mockResolvedValue({
311
205
  data: {
312
206
  orgs: [{ id: 1, login: 'github', description: 'How people build software', avatarUrl: '' }],
313
207
  },
314
208
  });
315
209
 
316
- mockGetRepo.execute.mockResolvedValue({
210
+ mockGetRepo.call.mockResolvedValue({
317
211
  data: {
318
212
  repo: {
319
213
  id: 1296269,
@@ -337,7 +231,7 @@ describe('GitHubReposOverviewWorkflow', () => {
337
231
  },
338
232
  });
339
233
 
340
- mockListBranches.execute.mockResolvedValue({
234
+ mockListBranches.call.mockResolvedValue({
341
235
  data: {
342
236
  branches: [
343
237
  { name: 'main', commitSha: 'abc123', protected: true },
@@ -346,7 +240,7 @@ describe('GitHubReposOverviewWorkflow', () => {
346
240
  },
347
241
  });
348
242
 
349
- mockListIssues.execute.mockResolvedValue({
243
+ mockListIssues.call.mockResolvedValue({
350
244
  data: {
351
245
  issues: [
352
246
  {
@@ -366,7 +260,7 @@ describe('GitHubReposOverviewWorkflow', () => {
366
260
  },
367
261
  });
368
262
 
369
- mockListPullRequests.execute.mockResolvedValue({
263
+ mockListPullRequests.call.mockResolvedValue({
370
264
  data: {
371
265
  pullRequests: [
372
266
  {
@@ -387,7 +281,7 @@ describe('GitHubReposOverviewWorkflow', () => {
387
281
  },
388
282
  });
389
283
 
390
- mockListDirectory.execute.mockResolvedValue({
284
+ mockListDirectory.call.mockResolvedValue({
391
285
  data: {
392
286
  entries: [
393
287
  { name: 'README.md', path: 'README.md', sha: 'abc', size: 1234, type: 'file', htmlUrl: '' },
@@ -396,7 +290,7 @@ describe('GitHubReposOverviewWorkflow', () => {
396
290
  },
397
291
  });
398
292
 
399
- mockListWorkflowRuns.execute.mockResolvedValue({
293
+ mockListWorkflowRuns.call.mockResolvedValue({
400
294
  data: {
401
295
  totalCount: 1,
402
296
  runs: [
@@ -416,7 +310,7 @@ describe('GitHubReposOverviewWorkflow', () => {
416
310
  },
417
311
  });
418
312
 
419
- mockSearchCode.execute.mockResolvedValue({
313
+ mockSearchCode.call.mockResolvedValue({
420
314
  data: {
421
315
  totalCount: 1,
422
316
  results: [
@@ -426,8 +320,8 @@ describe('GitHubReposOverviewWorkflow', () => {
426
320
  });
427
321
  }
428
322
 
429
- it('should execute full pipeline from start to end', async () => {
430
- const context = {} as RunContext;
323
+ it('should execute full workflow from start to end', async () => {
324
+ const context = createStatelessContext();
431
325
  setupAllMocks();
432
326
 
433
327
  const result = await processor.process(workflow, args, context);
@@ -436,131 +330,121 @@ describe('GitHubReposOverviewWorkflow', () => {
436
330
  expect(result.hasError).toBe(false);
437
331
  expect(result.place).toBe('end');
438
332
 
333
+ // Verify markdown document was created
334
+ expect(result.documents).toEqual(
335
+ expect.arrayContaining([expect.objectContaining({ className: 'MarkdownDocument' })]),
336
+ );
337
+
439
338
  // Verify all read tools were called
440
- expect(mockGetAuthenticatedUser.execute).toHaveBeenCalledTimes(1);
441
- expect(mockListUserOrgs.execute).toHaveBeenCalledTimes(1);
442
- expect(mockGetRepo.execute).toHaveBeenCalledTimes(1);
443
- expect(mockListBranches.execute).toHaveBeenCalledTimes(1);
444
- expect(mockListIssues.execute).toHaveBeenCalledTimes(1);
445
- expect(mockListPullRequests.execute).toHaveBeenCalledTimes(1);
446
- expect(mockListDirectory.execute).toHaveBeenCalledTimes(1);
447
- expect(mockListWorkflowRuns.execute).toHaveBeenCalledTimes(1);
448
- expect(mockSearchCode.execute).toHaveBeenCalledTimes(1);
449
- expect(mockCreateDocument.execute).toHaveBeenCalledTimes(1);
339
+ expect(mockGetAuthenticatedUser.call).toHaveBeenCalledTimes(1);
340
+ expect(mockListUserOrgs.call).toHaveBeenCalledTimes(1);
341
+ expect(mockGetRepo.call).toHaveBeenCalledTimes(1);
342
+ expect(mockListBranches.call).toHaveBeenCalledTimes(1);
343
+ expect(mockListIssues.call).toHaveBeenCalledTimes(1);
344
+ expect(mockListPullRequests.call).toHaveBeenCalledTimes(1);
345
+ expect(mockListDirectory.call).toHaveBeenCalledTimes(1);
346
+ expect(mockListWorkflowRuns.call).toHaveBeenCalledTimes(1);
347
+ expect(mockSearchCode.call).toHaveBeenCalledTimes(1);
450
348
  });
451
349
 
452
350
  it('should pass owner and repo to gitHubGetRepo', async () => {
453
- const context = {} as RunContext;
351
+ const context = createStatelessContext();
454
352
  setupAllMocks();
455
353
 
456
354
  await processor.process(workflow, args, context);
457
355
 
458
- expect(mockGetRepo.execute).toHaveBeenCalledWith(
356
+ expect(mockGetRepo.call).toHaveBeenCalledWith(
459
357
  expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
460
- expect.anything(),
461
- expect.anything(),
462
- expect.anything(),
358
+ undefined,
463
359
  );
464
360
  });
465
361
 
466
362
  it('should pass owner and repo to gitHubListBranches', async () => {
467
- const context = {} as RunContext;
363
+ const context = createStatelessContext();
468
364
  setupAllMocks();
469
365
 
470
366
  await processor.process(workflow, args, context);
471
367
 
472
- expect(mockListBranches.execute).toHaveBeenCalledWith(
368
+ expect(mockListBranches.call).toHaveBeenCalledWith(
473
369
  expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
474
- expect.anything(),
475
- expect.anything(),
476
- expect.anything(),
370
+ undefined,
477
371
  );
478
372
  });
479
373
 
480
374
  it('should pass owner and repo to gitHubListIssues with state open', async () => {
481
- const context = {} as RunContext;
375
+ const context = createStatelessContext();
482
376
  setupAllMocks();
483
377
 
484
378
  await processor.process(workflow, args, context);
485
379
 
486
- expect(mockListIssues.execute).toHaveBeenCalledWith(
380
+ expect(mockListIssues.call).toHaveBeenCalledWith(
487
381
  expect.objectContaining({ owner: 'octocat', repo: 'Hello-World', state: 'open' }),
488
- expect.anything(),
489
- expect.anything(),
490
- expect.anything(),
382
+ undefined,
491
383
  );
492
384
  });
493
385
 
494
386
  it('should pass owner and repo to gitHubListPullRequests', async () => {
495
- const context = {} as RunContext;
387
+ const context = createStatelessContext();
496
388
  setupAllMocks();
497
389
 
498
390
  await processor.process(workflow, args, context);
499
391
 
500
- expect(mockListPullRequests.execute).toHaveBeenCalledWith(
392
+ expect(mockListPullRequests.call).toHaveBeenCalledWith(
501
393
  expect.objectContaining({ owner: 'octocat', repo: 'Hello-World', state: 'open' }),
502
- expect.anything(),
503
- expect.anything(),
504
- expect.anything(),
394
+ undefined,
505
395
  );
506
396
  });
507
397
 
508
398
  it('should pass owner and repo to gitHubListDirectory', async () => {
509
- const context = {} as RunContext;
399
+ const context = createStatelessContext();
510
400
  setupAllMocks();
511
401
 
512
402
  await processor.process(workflow, args, context);
513
403
 
514
- expect(mockListDirectory.execute).toHaveBeenCalledWith(
404
+ expect(mockListDirectory.call).toHaveBeenCalledWith(
515
405
  expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
516
- expect.anything(),
517
- expect.anything(),
518
- expect.anything(),
406
+ undefined,
519
407
  );
520
408
  });
521
409
 
522
410
  it('should pass owner and repo to gitHubListWorkflowRuns', async () => {
523
- const context = {} as RunContext;
411
+ const context = createStatelessContext();
524
412
  setupAllMocks();
525
413
 
526
414
  await processor.process(workflow, args, context);
527
415
 
528
- expect(mockListWorkflowRuns.execute).toHaveBeenCalledWith(
416
+ expect(mockListWorkflowRuns.call).toHaveBeenCalledWith(
529
417
  expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
530
- expect.anything(),
531
- expect.anything(),
532
- expect.anything(),
418
+ undefined,
533
419
  );
534
420
  });
535
421
 
536
422
  it('should pass repo-scoped query to gitHubSearchCode', async () => {
537
- const context = {} as RunContext;
423
+ const context = createStatelessContext();
538
424
  setupAllMocks();
539
425
 
540
426
  await processor.process(workflow, args, context);
541
427
 
542
- expect(mockSearchCode.execute).toHaveBeenCalledWith(
428
+ expect(mockSearchCode.call).toHaveBeenCalledWith(
543
429
  expect.objectContaining({ query: 'repo:octocat/Hello-World' }),
544
- expect.anything(),
545
- expect.anything(),
546
- expect.anything(),
430
+ undefined,
547
431
  );
548
432
  });
549
433
  });
550
434
 
551
435
  describe('workflow execution — auth flow', () => {
552
436
  it('should stop at awaiting_auth when unauthorized', async () => {
553
- const context = {} as RunContext;
437
+ const context = createStatelessContext();
554
438
 
555
- mockGetAuthenticatedUser.execute.mockResolvedValue({
439
+ mockGetAuthenticatedUser.call.mockResolvedValue({
556
440
  data: {
557
441
  error: 'unauthorized',
558
442
  message: 'No valid GitHub token found. Please authenticate first.',
559
443
  },
560
444
  });
561
445
 
562
- mockTask.execute.mockResolvedValue({
563
- data: { pipelineId: 'test-pipeline-id' },
446
+ mockOAuthWorkflow.run.mockResolvedValue({
447
+ workflowId: 'test-workflow-id',
564
448
  });
565
449
 
566
450
  const result = await processor.process(workflow, { owner: 'octocat', repo: 'Hello-World' }, context);
@@ -570,25 +454,19 @@ describe('GitHubReposOverviewWorkflow', () => {
570
454
  expect(result.stop).toBe(true);
571
455
  expect(result.place).toBe('awaiting_auth');
572
456
 
573
- expect(mockGetAuthenticatedUser.execute).toHaveBeenCalledTimes(1);
574
- expect(mockTask.execute).toHaveBeenCalledTimes(1);
575
- expect(mockTask.execute).toHaveBeenCalledWith(
457
+ expect(mockGetAuthenticatedUser.call).toHaveBeenCalledTimes(1);
458
+ expect(mockOAuthWorkflow.run).toHaveBeenCalledTimes(1);
459
+ expect(mockOAuthWorkflow.run).toHaveBeenCalledWith(
460
+ { provider: 'github', scopes: ['repo', 'read:org', 'workflow'] },
576
461
  expect.objectContaining({
577
- workflow: 'oAuth',
578
- args: expect.objectContaining({
579
- provider: 'github',
580
- scopes: ['repo', 'read:org', 'workflow'],
581
- }),
582
- callback: { transition: 'auth_completed' },
462
+ alias: 'oAuth',
463
+ callback: { transition: 'authCompleted' },
583
464
  }),
584
- expect.anything(),
585
- expect.anything(),
586
- expect.anything(),
587
465
  );
588
466
 
589
467
  // Subsequent tools should NOT be called
590
- expect(mockListUserOrgs.execute).not.toHaveBeenCalled();
591
- expect(mockGetRepo.execute).not.toHaveBeenCalled();
468
+ expect(mockListUserOrgs.call).not.toHaveBeenCalled();
469
+ expect(mockGetRepo.call).not.toHaveBeenCalled();
592
470
  });
593
471
  });
594
472
  });
@@ -596,17 +474,6 @@ describe('GitHubReposOverviewWorkflow', () => {
596
474
  describe('GitHubReposOverviewWorkflow with existing entity', () => {
597
475
  let module: TestingModule;
598
476
 
599
- function buildWorkflowTest() {
600
- return applyAllGitHubToolMocks(
601
- createWorkflowTest()
602
- .forWorkflow(GitHubReposOverviewWorkflow)
603
- .withImports(LoopCoreModule, CreateChatMessageToolModule, OAuthModule)
604
- .withToolOverride(Task)
605
- .withToolOverride(CreateDocument)
606
- .withToolOverride(CreateChatMessage),
607
- );
608
- }
609
-
610
477
  afterEach(async () => {
611
478
  if (module) {
612
479
  await module.close();
@@ -617,16 +484,9 @@ describe('GitHubReposOverviewWorkflow with existing entity', () => {
617
484
  const workflowId = '00000000-0000-0000-0000-000000000001';
618
485
  const args = { owner: 'octocat', repo: 'Hello-World' };
619
486
 
620
- module = await buildWorkflowTest()
621
- .withExistingWorkflow({
622
- place: 'awaiting_auth',
623
- inputData: args,
624
- id: workflowId,
625
- hashRecord: {
626
- options: generateObjectFingerprint(args),
627
- },
628
- })
629
- .compile();
487
+ jest.clearAllMocks();
488
+
489
+ module = await buildWorkflowTest().compile();
630
490
 
631
491
  const workflow = module.get(GitHubReposOverviewWorkflow);
632
492
  const processor = module.get(WorkflowProcessorService);
@@ -639,10 +499,9 @@ describe('GitHubReposOverviewWorkflow with existing entity', () => {
639
499
  const mockListDirectory: ToolMock = module.get(GitHubListDirectoryTool);
640
500
  const mockListWorkflowRuns: ToolMock = module.get(GitHubListWorkflowRunsTool);
641
501
  const mockSearchCode: ToolMock = module.get(GitHubSearchCodeTool);
642
- const mockCreateDocument: ToolMock = module.get(CreateDocument);
643
502
 
644
503
  // After auth, the workflow retries from start and succeeds
645
- mockGetAuthenticatedUser.execute.mockResolvedValue({
504
+ mockGetAuthenticatedUser.call.mockResolvedValue({
646
505
  data: {
647
506
  user: {
648
507
  id: 1,
@@ -659,8 +518,8 @@ describe('GitHubReposOverviewWorkflow with existing entity', () => {
659
518
  },
660
519
  },
661
520
  });
662
- mockListUserOrgs.execute.mockResolvedValue({ data: { orgs: [] } });
663
- mockGetRepo.execute.mockResolvedValue({
521
+ mockListUserOrgs.call.mockResolvedValue({ data: { orgs: [] } });
522
+ mockGetRepo.call.mockResolvedValue({
664
523
  data: {
665
524
  repo: {
666
525
  id: 1,
@@ -683,19 +542,24 @@ describe('GitHubReposOverviewWorkflow with existing entity', () => {
683
542
  },
684
543
  },
685
544
  });
686
- mockListBranches.execute.mockResolvedValue({ data: { branches: [] } });
687
- mockListIssues.execute.mockResolvedValue({ data: { issues: [] } });
688
- mockListPullRequests.execute.mockResolvedValue({ data: { pullRequests: [] } });
689
- mockListDirectory.execute.mockResolvedValue({ data: { entries: [] } });
690
- mockListWorkflowRuns.execute.mockResolvedValue({ data: { totalCount: 0, runs: [] } });
691
- mockSearchCode.execute.mockResolvedValue({ data: { totalCount: 0, results: [] } });
545
+ mockListBranches.call.mockResolvedValue({ data: { branches: [] } });
546
+ mockListIssues.call.mockResolvedValue({ data: { issues: [] } });
547
+ mockListPullRequests.call.mockResolvedValue({ data: { pullRequests: [] } });
548
+ mockListDirectory.call.mockResolvedValue({ data: { entries: [] } });
549
+ mockListWorkflowRuns.call.mockResolvedValue({ data: { totalCount: 0, runs: [] } });
550
+ mockSearchCode.call.mockResolvedValue({ data: { totalCount: 0, results: [] } });
692
551
 
693
552
  const context = {
553
+ workflowEntity: {
554
+ id: workflowId,
555
+ place: 'awaiting_auth',
556
+ documents: [],
557
+ } as Partial<WorkflowEntity>,
694
558
  payload: {
695
559
  transition: {
696
- id: 'auth_completed',
560
+ id: 'authCompleted',
697
561
  workflowId,
698
- payload: { pipelineId: 'auth-pipeline-id' },
562
+ payload: { workflowId: 'auth-workflow-id', status: 'completed', data: {} },
699
563
  },
700
564
  },
701
565
  } as unknown as RunContext;
@@ -706,20 +570,12 @@ describe('GitHubReposOverviewWorkflow with existing entity', () => {
706
570
  expect(result.hasError).toBe(false);
707
571
  expect(result.place).toBe('end');
708
572
 
709
- expect(mockCreateDocument.execute).toHaveBeenCalled();
710
- expect(mockGetAuthenticatedUser.execute).toHaveBeenCalledTimes(1);
711
- expect(mockGetRepo.execute).toHaveBeenCalledTimes(1);
712
- });
713
-
714
- it('should resume from existing workflow state', async () => {
715
- module = await buildWorkflowTest()
716
- .withExistingWorkflow({
717
- place: 'repo_fetched',
718
- inputData: { owner: 'octocat', repo: 'Hello-World' },
719
- })
720
- .compile();
573
+ // Verify markdown document was created after auth resume
574
+ expect(result.documents).toEqual(
575
+ expect.arrayContaining([expect.objectContaining({ className: 'MarkdownDocument' })]),
576
+ );
721
577
 
722
- const workflow = module.get(GitHubReposOverviewWorkflow);
723
- expect(workflow).toBeDefined();
578
+ expect(mockGetAuthenticatedUser.call).toHaveBeenCalledTimes(1);
579
+ expect(mockGetRepo.call).toHaveBeenCalledTimes(1);
724
580
  });
725
581
  });
@@ -0,0 +1,16 @@
1
+ title: 'GitHub Agent'
2
+
3
+ description: |
4
+ An interactive chat agent with access to GitHub.
5
+ Ask it to manage repositories, issues, pull requests, browse code, check CI/CD status,
6
+ search across GitHub, and more. Handles OAuth authentication automatically when needed —
7
+ the agent detects unauthorized errors and launches authentication on its own.
8
+
9
+ ui:
10
+ widgets:
11
+ - widget: prompt-input
12
+ enabledWhen:
13
+ - waiting_for_user
14
+ options:
15
+ transition: userMessage
16
+ label: Send Message