@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.
- package/README.md +116 -20
- package/dist/tools/authenticate-github-task.tool.d.ts +9 -8
- package/dist/tools/authenticate-github-task.tool.d.ts.map +1 -1
- package/dist/tools/authenticate-github-task.tool.js +30 -62
- package/dist/tools/authenticate-github-task.tool.js.map +1 -1
- package/dist/workflows/github-agent.ui.yaml +16 -0
- package/dist/workflows/github-agent.workflow.d.ts +14 -13
- package/dist/workflows/github-agent.workflow.d.ts.map +1 -1
- package/dist/workflows/github-agent.workflow.js +155 -71
- package/dist/workflows/github-agent.workflow.js.map +1 -1
- package/dist/workflows/github-repos-overview.ui.yaml +17 -0
- package/dist/workflows/github-repos-overview.workflow.d.ts +77 -87
- package/dist/workflows/github-repos-overview.workflow.d.ts.map +1 -1
- package/dist/workflows/github-repos-overview.workflow.js +177 -228
- package/dist/workflows/github-repos-overview.workflow.js.map +1 -1
- package/dist/workflows/templates/repoOverview.md +81 -0
- package/dist/workflows/templates/systemMessage.md +23 -0
- package/package.json +7 -7
- package/src/tools/authenticate-github-task.tool.ts +34 -82
- package/src/workflows/__tests__/github-repos-overview-workflow.spec.ts +105 -249
- package/src/workflows/github-agent.ui.yaml +16 -0
- package/src/workflows/github-agent.workflow.ts +109 -27
- package/src/workflows/github-repos-overview.ui.yaml +17 -0
- package/src/workflows/github-repos-overview.workflow.ts +257 -215
- package/src/workflows/templates/repoOverview.md +81 -0
- package/src/workflows/templates/systemMessage.md +23 -0
- package/dist/workflows/github-agent.workflow.yaml +0 -154
- package/dist/workflows/github-repos-overview.workflow.yaml +0 -249
- package/src/workflows/github-agent.workflow.yaml +0 -154
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
430
|
-
const context =
|
|
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.
|
|
441
|
-
expect(mockListUserOrgs.
|
|
442
|
-
expect(mockGetRepo.
|
|
443
|
-
expect(mockListBranches.
|
|
444
|
-
expect(mockListIssues.
|
|
445
|
-
expect(mockListPullRequests.
|
|
446
|
-
expect(mockListDirectory.
|
|
447
|
-
expect(mockListWorkflowRuns.
|
|
448
|
-
expect(mockSearchCode.
|
|
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 =
|
|
351
|
+
const context = createStatelessContext();
|
|
454
352
|
setupAllMocks();
|
|
455
353
|
|
|
456
354
|
await processor.process(workflow, args, context);
|
|
457
355
|
|
|
458
|
-
expect(mockGetRepo.
|
|
356
|
+
expect(mockGetRepo.call).toHaveBeenCalledWith(
|
|
459
357
|
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
460
|
-
|
|
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 =
|
|
363
|
+
const context = createStatelessContext();
|
|
468
364
|
setupAllMocks();
|
|
469
365
|
|
|
470
366
|
await processor.process(workflow, args, context);
|
|
471
367
|
|
|
472
|
-
expect(mockListBranches.
|
|
368
|
+
expect(mockListBranches.call).toHaveBeenCalledWith(
|
|
473
369
|
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
474
|
-
|
|
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 =
|
|
375
|
+
const context = createStatelessContext();
|
|
482
376
|
setupAllMocks();
|
|
483
377
|
|
|
484
378
|
await processor.process(workflow, args, context);
|
|
485
379
|
|
|
486
|
-
expect(mockListIssues.
|
|
380
|
+
expect(mockListIssues.call).toHaveBeenCalledWith(
|
|
487
381
|
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World', state: 'open' }),
|
|
488
|
-
|
|
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 =
|
|
387
|
+
const context = createStatelessContext();
|
|
496
388
|
setupAllMocks();
|
|
497
389
|
|
|
498
390
|
await processor.process(workflow, args, context);
|
|
499
391
|
|
|
500
|
-
expect(mockListPullRequests.
|
|
392
|
+
expect(mockListPullRequests.call).toHaveBeenCalledWith(
|
|
501
393
|
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World', state: 'open' }),
|
|
502
|
-
|
|
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 =
|
|
399
|
+
const context = createStatelessContext();
|
|
510
400
|
setupAllMocks();
|
|
511
401
|
|
|
512
402
|
await processor.process(workflow, args, context);
|
|
513
403
|
|
|
514
|
-
expect(mockListDirectory.
|
|
404
|
+
expect(mockListDirectory.call).toHaveBeenCalledWith(
|
|
515
405
|
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
516
|
-
|
|
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 =
|
|
411
|
+
const context = createStatelessContext();
|
|
524
412
|
setupAllMocks();
|
|
525
413
|
|
|
526
414
|
await processor.process(workflow, args, context);
|
|
527
415
|
|
|
528
|
-
expect(mockListWorkflowRuns.
|
|
416
|
+
expect(mockListWorkflowRuns.call).toHaveBeenCalledWith(
|
|
529
417
|
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
530
|
-
|
|
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 =
|
|
423
|
+
const context = createStatelessContext();
|
|
538
424
|
setupAllMocks();
|
|
539
425
|
|
|
540
426
|
await processor.process(workflow, args, context);
|
|
541
427
|
|
|
542
|
-
expect(mockSearchCode.
|
|
428
|
+
expect(mockSearchCode.call).toHaveBeenCalledWith(
|
|
543
429
|
expect.objectContaining({ query: 'repo:octocat/Hello-World' }),
|
|
544
|
-
|
|
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 =
|
|
437
|
+
const context = createStatelessContext();
|
|
554
438
|
|
|
555
|
-
mockGetAuthenticatedUser.
|
|
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
|
-
|
|
563
|
-
|
|
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.
|
|
574
|
-
expect(
|
|
575
|
-
expect(
|
|
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
|
-
|
|
578
|
-
|
|
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.
|
|
591
|
-
expect(mockGetRepo.
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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.
|
|
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.
|
|
663
|
-
mockGetRepo.
|
|
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.
|
|
687
|
-
mockListIssues.
|
|
688
|
-
mockListPullRequests.
|
|
689
|
-
mockListDirectory.
|
|
690
|
-
mockListWorkflowRuns.
|
|
691
|
-
mockSearchCode.
|
|
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: '
|
|
560
|
+
id: 'authCompleted',
|
|
697
561
|
workflowId,
|
|
698
|
-
payload: {
|
|
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
|
-
|
|
710
|
-
expect(
|
|
711
|
-
|
|
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
|
-
|
|
723
|
-
expect(
|
|
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
|