@loopstack/github-oauth-example 0.1.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 +114 -0
- package/dist/github-example.module.d.ts +3 -0
- package/dist/github-example.module.d.ts.map +1 -0
- package/dist/github-example.module.js +27 -0
- package/dist/github-example.module.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/authenticate-github-task.tool.d.ts +17 -0
- package/dist/tools/authenticate-github-task.tool.d.ts.map +1 -0
- package/dist/tools/authenticate-github-task.tool.js +107 -0
- package/dist/tools/authenticate-github-task.tool.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/workflows/github-agent.workflow.d.ts +48 -0
- package/dist/workflows/github-agent.workflow.d.ts.map +1 -0
- package/dist/workflows/github-agent.workflow.js +215 -0
- package/dist/workflows/github-agent.workflow.js.map +1 -0
- package/dist/workflows/github-agent.workflow.yaml +154 -0
- package/dist/workflows/github-repos-overview.workflow.d.ts +102 -0
- package/dist/workflows/github-repos-overview.workflow.d.ts.map +1 -0
- package/dist/workflows/github-repos-overview.workflow.js +300 -0
- package/dist/workflows/github-repos-overview.workflow.js.map +1 -0
- package/dist/workflows/github-repos-overview.workflow.yaml +249 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +19 -0
- package/dist/workflows/index.js.map +1 -0
- package/package.json +80 -0
- package/src/github-example.module.ts +14 -0
- package/src/index.ts +3 -0
- package/src/tools/authenticate-github-task.tool.ts +125 -0
- package/src/tools/index.ts +1 -0
- package/src/workflows/__tests__/github-repos-overview-workflow.spec.ts +725 -0
- package/src/workflows/github-agent.workflow.ts +118 -0
- package/src/workflows/github-agent.workflow.yaml +154 -0
- package/src/workflows/github-repos-overview.workflow.ts +254 -0
- package/src/workflows/github-repos-overview.workflow.yaml +249 -0
- package/src/workflows/index.ts +2 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { TestingModule } from '@nestjs/testing';
|
|
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';
|
|
15
|
+
import { CreateChatMessage, CreateChatMessageToolModule } from '@loopstack/create-chat-message-tool';
|
|
16
|
+
import {
|
|
17
|
+
GitHubCreateIssueCommentTool,
|
|
18
|
+
GitHubCreateIssueTool,
|
|
19
|
+
GitHubCreateOrUpdateFileTool,
|
|
20
|
+
GitHubCreatePullRequestTool,
|
|
21
|
+
GitHubCreateRepoTool,
|
|
22
|
+
GitHubGetAuthenticatedUserTool,
|
|
23
|
+
GitHubGetCommitTool,
|
|
24
|
+
GitHubGetFileContentTool,
|
|
25
|
+
GitHubGetIssueTool,
|
|
26
|
+
GitHubGetPullRequestTool,
|
|
27
|
+
GitHubGetRepoTool,
|
|
28
|
+
GitHubGetWorkflowRunTool,
|
|
29
|
+
GitHubListBranchesTool,
|
|
30
|
+
GitHubListDirectoryTool,
|
|
31
|
+
GitHubListIssuesTool,
|
|
32
|
+
GitHubListPrReviewsTool,
|
|
33
|
+
GitHubListPullRequestsTool,
|
|
34
|
+
GitHubListReposTool,
|
|
35
|
+
GitHubListUserOrgsTool,
|
|
36
|
+
GitHubListWorkflowRunsTool,
|
|
37
|
+
GitHubMergePullRequestTool,
|
|
38
|
+
GitHubSearchCodeTool,
|
|
39
|
+
GitHubSearchIssuesTool,
|
|
40
|
+
GitHubSearchReposTool,
|
|
41
|
+
GitHubTriggerWorkflowTool,
|
|
42
|
+
} from '@loopstack/github-module';
|
|
43
|
+
import { OAuthModule } from '@loopstack/oauth-module';
|
|
44
|
+
import { ToolMock, createWorkflowTest } from '@loopstack/testing';
|
|
45
|
+
import { GitHubReposOverviewWorkflow } from '../github-repos-overview.workflow';
|
|
46
|
+
|
|
47
|
+
function applyAllGitHubToolMocks(builder: ReturnType<typeof createWorkflowTest>) {
|
|
48
|
+
return builder
|
|
49
|
+
.withToolMock(GitHubGetAuthenticatedUserTool)
|
|
50
|
+
.withToolMock(GitHubListUserOrgsTool)
|
|
51
|
+
.withToolMock(GitHubListReposTool)
|
|
52
|
+
.withToolMock(GitHubGetRepoTool)
|
|
53
|
+
.withToolMock(GitHubCreateRepoTool)
|
|
54
|
+
.withToolMock(GitHubListBranchesTool)
|
|
55
|
+
.withToolMock(GitHubListIssuesTool)
|
|
56
|
+
.withToolMock(GitHubGetIssueTool)
|
|
57
|
+
.withToolMock(GitHubCreateIssueTool)
|
|
58
|
+
.withToolMock(GitHubCreateIssueCommentTool)
|
|
59
|
+
.withToolMock(GitHubListPullRequestsTool)
|
|
60
|
+
.withToolMock(GitHubGetPullRequestTool)
|
|
61
|
+
.withToolMock(GitHubCreatePullRequestTool)
|
|
62
|
+
.withToolMock(GitHubMergePullRequestTool)
|
|
63
|
+
.withToolMock(GitHubListPrReviewsTool)
|
|
64
|
+
.withToolMock(GitHubGetFileContentTool)
|
|
65
|
+
.withToolMock(GitHubCreateOrUpdateFileTool)
|
|
66
|
+
.withToolMock(GitHubListDirectoryTool)
|
|
67
|
+
.withToolMock(GitHubGetCommitTool)
|
|
68
|
+
.withToolMock(GitHubListWorkflowRunsTool)
|
|
69
|
+
.withToolMock(GitHubTriggerWorkflowTool)
|
|
70
|
+
.withToolMock(GitHubGetWorkflowRunTool)
|
|
71
|
+
.withToolMock(GitHubSearchCodeTool)
|
|
72
|
+
.withToolMock(GitHubSearchReposTool)
|
|
73
|
+
.withToolMock(GitHubSearchIssuesTool);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('GitHubReposOverviewWorkflow', () => {
|
|
77
|
+
let module: TestingModule;
|
|
78
|
+
let workflow: GitHubReposOverviewWorkflow;
|
|
79
|
+
let processor: WorkflowProcessorService;
|
|
80
|
+
|
|
81
|
+
let mockGetAuthenticatedUser: ToolMock;
|
|
82
|
+
let mockListUserOrgs: ToolMock;
|
|
83
|
+
let mockGetRepo: ToolMock;
|
|
84
|
+
let mockListBranches: ToolMock;
|
|
85
|
+
let mockListIssues: ToolMock;
|
|
86
|
+
let mockListPullRequests: ToolMock;
|
|
87
|
+
let mockListDirectory: ToolMock;
|
|
88
|
+
let mockListWorkflowRuns: ToolMock;
|
|
89
|
+
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
|
+
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
module = await buildWorkflowTest().compile();
|
|
106
|
+
|
|
107
|
+
workflow = module.get(GitHubReposOverviewWorkflow);
|
|
108
|
+
processor = module.get(WorkflowProcessorService);
|
|
109
|
+
|
|
110
|
+
mockGetAuthenticatedUser = module.get(GitHubGetAuthenticatedUserTool);
|
|
111
|
+
mockListUserOrgs = module.get(GitHubListUserOrgsTool);
|
|
112
|
+
mockGetRepo = module.get(GitHubGetRepoTool);
|
|
113
|
+
mockListBranches = module.get(GitHubListBranchesTool);
|
|
114
|
+
mockListIssues = module.get(GitHubListIssuesTool);
|
|
115
|
+
mockListPullRequests = module.get(GitHubListPullRequestsTool);
|
|
116
|
+
mockListDirectory = module.get(GitHubListDirectoryTool);
|
|
117
|
+
mockListWorkflowRuns = module.get(GitHubListWorkflowRunsTool);
|
|
118
|
+
mockSearchCode = module.get(GitHubSearchCodeTool);
|
|
119
|
+
mockTask = module.get(Task);
|
|
120
|
+
mockCreateDocument = module.get(CreateDocument);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterEach(async () => {
|
|
124
|
+
if (module) {
|
|
125
|
+
await module.close();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('initialization', () => {
|
|
130
|
+
it('should be defined', () => {
|
|
131
|
+
expect(workflow).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should have argsSchema defined', () => {
|
|
135
|
+
expect(getBlockArgsSchema(workflow)).toBeDefined();
|
|
136
|
+
expect(getBlockArgsSchema(workflow)).toBeInstanceOf(z.ZodType);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should have stateSchema defined', () => {
|
|
140
|
+
expect(getBlockStateSchema(workflow)).toBeDefined();
|
|
141
|
+
expect(getBlockStateSchema(workflow)).toBeInstanceOf(z.ZodType);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should have config defined', () => {
|
|
145
|
+
expect(getBlockConfig(workflow)).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should have all 28 tools available', () => {
|
|
149
|
+
const tools = getBlockTools(workflow);
|
|
150
|
+
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
|
+
|
|
158
|
+
// GitHub — Users
|
|
159
|
+
expect(tools).toContain('gitHubGetAuthenticatedUser');
|
|
160
|
+
expect(tools).toContain('gitHubListUserOrgs');
|
|
161
|
+
|
|
162
|
+
// GitHub — Repos
|
|
163
|
+
expect(tools).toContain('gitHubListRepos');
|
|
164
|
+
expect(tools).toContain('gitHubGetRepo');
|
|
165
|
+
expect(tools).toContain('gitHubCreateRepo');
|
|
166
|
+
expect(tools).toContain('gitHubListBranches');
|
|
167
|
+
|
|
168
|
+
// GitHub — Issues
|
|
169
|
+
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
|
+
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
|
+
expect(tools).toContain('gitHubListDirectory');
|
|
185
|
+
expect(tools).toContain('gitHubGetCommit');
|
|
186
|
+
|
|
187
|
+
// GitHub — Actions
|
|
188
|
+
expect(tools).toContain('gitHubListWorkflowRuns');
|
|
189
|
+
expect(tools).toContain('gitHubTriggerWorkflow');
|
|
190
|
+
expect(tools).toContain('gitHubGetWorkflowRun');
|
|
191
|
+
|
|
192
|
+
// GitHub — Search
|
|
193
|
+
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
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('arguments', () => {
|
|
208
|
+
it('should validate arguments with owner and repo', () => {
|
|
209
|
+
const schema = getBlockArgsSchema(workflow)!;
|
|
210
|
+
const result = schema.parse({ owner: 'octocat', repo: 'Hello-World' });
|
|
211
|
+
expect(result).toEqual({ owner: 'octocat', repo: 'Hello-World' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should apply default owner when missing', () => {
|
|
215
|
+
const schema = getBlockArgsSchema(workflow)!;
|
|
216
|
+
const result = schema.parse({ repo: 'Hello-World' });
|
|
217
|
+
expect(result).toEqual({ owner: 'octocat', repo: 'Hello-World' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should apply default repo when missing', () => {
|
|
221
|
+
const schema = getBlockArgsSchema(workflow)!;
|
|
222
|
+
const result = schema.parse({ owner: 'myuser' });
|
|
223
|
+
expect(result).toEqual({ owner: 'myuser', repo: 'Hello-World' });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should apply all defaults when empty', () => {
|
|
227
|
+
const schema = getBlockArgsSchema(workflow)!;
|
|
228
|
+
const result = schema.parse({});
|
|
229
|
+
expect(result).toEqual({ owner: 'octocat', repo: 'Hello-World' });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should reject extra properties (strict mode)', () => {
|
|
233
|
+
const schema = getBlockArgsSchema(workflow)!;
|
|
234
|
+
expect(() => schema.parse({ owner: 'octocat', repo: 'Hello-World', extra: 'nope' })).toThrow();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
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
|
+
describe('workflow execution — happy path', () => {
|
|
289
|
+
const args = { owner: 'octocat', repo: 'Hello-World' };
|
|
290
|
+
|
|
291
|
+
function setupAllMocks() {
|
|
292
|
+
mockGetAuthenticatedUser.execute.mockResolvedValue({
|
|
293
|
+
data: {
|
|
294
|
+
user: {
|
|
295
|
+
id: 1,
|
|
296
|
+
login: 'octocat',
|
|
297
|
+
name: 'The Octocat',
|
|
298
|
+
email: null,
|
|
299
|
+
avatarUrl: '',
|
|
300
|
+
htmlUrl: 'https://github.com/octocat',
|
|
301
|
+
bio: null,
|
|
302
|
+
publicRepos: 8,
|
|
303
|
+
followers: 100,
|
|
304
|
+
following: 0,
|
|
305
|
+
createdAt: '2011-01-25T00:00:00Z',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
mockListUserOrgs.execute.mockResolvedValue({
|
|
311
|
+
data: {
|
|
312
|
+
orgs: [{ id: 1, login: 'github', description: 'How people build software', avatarUrl: '' }],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
mockGetRepo.execute.mockResolvedValue({
|
|
317
|
+
data: {
|
|
318
|
+
repo: {
|
|
319
|
+
id: 1296269,
|
|
320
|
+
fullName: 'octocat/Hello-World',
|
|
321
|
+
name: 'Hello-World',
|
|
322
|
+
owner: 'octocat',
|
|
323
|
+
ownerAvatar: '',
|
|
324
|
+
private: false,
|
|
325
|
+
htmlUrl: 'https://github.com/octocat/Hello-World',
|
|
326
|
+
description: 'My first repository on GitHub!',
|
|
327
|
+
language: 'JavaScript',
|
|
328
|
+
defaultBranch: 'main',
|
|
329
|
+
stars: 2500,
|
|
330
|
+
forks: 1800,
|
|
331
|
+
openIssues: 42,
|
|
332
|
+
createdAt: '2011-01-26T00:00:00Z',
|
|
333
|
+
updatedAt: '2025-01-15T00:00:00Z',
|
|
334
|
+
topics: [],
|
|
335
|
+
license: 'MIT',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
mockListBranches.execute.mockResolvedValue({
|
|
341
|
+
data: {
|
|
342
|
+
branches: [
|
|
343
|
+
{ name: 'main', commitSha: 'abc123', protected: true },
|
|
344
|
+
{ name: 'develop', commitSha: 'def456', protected: false },
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
mockListIssues.execute.mockResolvedValue({
|
|
350
|
+
data: {
|
|
351
|
+
issues: [
|
|
352
|
+
{
|
|
353
|
+
id: 1,
|
|
354
|
+
number: 42,
|
|
355
|
+
title: 'Found a bug',
|
|
356
|
+
state: 'open',
|
|
357
|
+
user: 'octocat',
|
|
358
|
+
labels: ['bug'],
|
|
359
|
+
assignees: [],
|
|
360
|
+
createdAt: '2025-01-10T00:00:00Z',
|
|
361
|
+
updatedAt: '2025-01-10T00:00:00Z',
|
|
362
|
+
htmlUrl: 'https://github.com/octocat/Hello-World/issues/42',
|
|
363
|
+
isPullRequest: false,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
mockListPullRequests.execute.mockResolvedValue({
|
|
370
|
+
data: {
|
|
371
|
+
pullRequests: [
|
|
372
|
+
{
|
|
373
|
+
id: 1,
|
|
374
|
+
number: 10,
|
|
375
|
+
title: 'Add feature',
|
|
376
|
+
state: 'open',
|
|
377
|
+
user: 'octocat',
|
|
378
|
+
head: 'feature',
|
|
379
|
+
headSha: 'abc',
|
|
380
|
+
base: 'main',
|
|
381
|
+
createdAt: '2025-01-12T00:00:00Z',
|
|
382
|
+
updatedAt: '2025-01-12T00:00:00Z',
|
|
383
|
+
htmlUrl: 'https://github.com/octocat/Hello-World/pull/10',
|
|
384
|
+
draft: false,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
mockListDirectory.execute.mockResolvedValue({
|
|
391
|
+
data: {
|
|
392
|
+
entries: [
|
|
393
|
+
{ name: 'README.md', path: 'README.md', sha: 'abc', size: 1234, type: 'file', htmlUrl: '' },
|
|
394
|
+
{ name: 'src', path: 'src', sha: 'def', size: 0, type: 'dir', htmlUrl: '' },
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
mockListWorkflowRuns.execute.mockResolvedValue({
|
|
400
|
+
data: {
|
|
401
|
+
totalCount: 1,
|
|
402
|
+
runs: [
|
|
403
|
+
{
|
|
404
|
+
id: 100,
|
|
405
|
+
name: 'CI',
|
|
406
|
+
status: 'completed',
|
|
407
|
+
conclusion: 'success',
|
|
408
|
+
headBranch: 'main',
|
|
409
|
+
headSha: 'abc',
|
|
410
|
+
event: 'push',
|
|
411
|
+
createdAt: '2025-01-14T00:00:00Z',
|
|
412
|
+
updatedAt: '2025-01-14T00:00:00Z',
|
|
413
|
+
htmlUrl: 'https://github.com/octocat/Hello-World/actions/runs/100',
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
mockSearchCode.execute.mockResolvedValue({
|
|
420
|
+
data: {
|
|
421
|
+
totalCount: 1,
|
|
422
|
+
results: [
|
|
423
|
+
{ name: 'index.js', path: 'src/index.js', sha: 'xyz', htmlUrl: '', repository: 'octocat/Hello-World' },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
it('should execute full pipeline from start to end', async () => {
|
|
430
|
+
const context = {} as RunContext;
|
|
431
|
+
setupAllMocks();
|
|
432
|
+
|
|
433
|
+
const result = await processor.process(workflow, args, context);
|
|
434
|
+
|
|
435
|
+
expect(result).toBeDefined();
|
|
436
|
+
expect(result.hasError).toBe(false);
|
|
437
|
+
expect(result.place).toBe('end');
|
|
438
|
+
|
|
439
|
+
// 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);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should pass owner and repo to gitHubGetRepo', async () => {
|
|
453
|
+
const context = {} as RunContext;
|
|
454
|
+
setupAllMocks();
|
|
455
|
+
|
|
456
|
+
await processor.process(workflow, args, context);
|
|
457
|
+
|
|
458
|
+
expect(mockGetRepo.execute).toHaveBeenCalledWith(
|
|
459
|
+
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
460
|
+
expect.anything(),
|
|
461
|
+
expect.anything(),
|
|
462
|
+
expect.anything(),
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should pass owner and repo to gitHubListBranches', async () => {
|
|
467
|
+
const context = {} as RunContext;
|
|
468
|
+
setupAllMocks();
|
|
469
|
+
|
|
470
|
+
await processor.process(workflow, args, context);
|
|
471
|
+
|
|
472
|
+
expect(mockListBranches.execute).toHaveBeenCalledWith(
|
|
473
|
+
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
474
|
+
expect.anything(),
|
|
475
|
+
expect.anything(),
|
|
476
|
+
expect.anything(),
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should pass owner and repo to gitHubListIssues with state open', async () => {
|
|
481
|
+
const context = {} as RunContext;
|
|
482
|
+
setupAllMocks();
|
|
483
|
+
|
|
484
|
+
await processor.process(workflow, args, context);
|
|
485
|
+
|
|
486
|
+
expect(mockListIssues.execute).toHaveBeenCalledWith(
|
|
487
|
+
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World', state: 'open' }),
|
|
488
|
+
expect.anything(),
|
|
489
|
+
expect.anything(),
|
|
490
|
+
expect.anything(),
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should pass owner and repo to gitHubListPullRequests', async () => {
|
|
495
|
+
const context = {} as RunContext;
|
|
496
|
+
setupAllMocks();
|
|
497
|
+
|
|
498
|
+
await processor.process(workflow, args, context);
|
|
499
|
+
|
|
500
|
+
expect(mockListPullRequests.execute).toHaveBeenCalledWith(
|
|
501
|
+
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World', state: 'open' }),
|
|
502
|
+
expect.anything(),
|
|
503
|
+
expect.anything(),
|
|
504
|
+
expect.anything(),
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should pass owner and repo to gitHubListDirectory', async () => {
|
|
509
|
+
const context = {} as RunContext;
|
|
510
|
+
setupAllMocks();
|
|
511
|
+
|
|
512
|
+
await processor.process(workflow, args, context);
|
|
513
|
+
|
|
514
|
+
expect(mockListDirectory.execute).toHaveBeenCalledWith(
|
|
515
|
+
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
516
|
+
expect.anything(),
|
|
517
|
+
expect.anything(),
|
|
518
|
+
expect.anything(),
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should pass owner and repo to gitHubListWorkflowRuns', async () => {
|
|
523
|
+
const context = {} as RunContext;
|
|
524
|
+
setupAllMocks();
|
|
525
|
+
|
|
526
|
+
await processor.process(workflow, args, context);
|
|
527
|
+
|
|
528
|
+
expect(mockListWorkflowRuns.execute).toHaveBeenCalledWith(
|
|
529
|
+
expect.objectContaining({ owner: 'octocat', repo: 'Hello-World' }),
|
|
530
|
+
expect.anything(),
|
|
531
|
+
expect.anything(),
|
|
532
|
+
expect.anything(),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should pass repo-scoped query to gitHubSearchCode', async () => {
|
|
537
|
+
const context = {} as RunContext;
|
|
538
|
+
setupAllMocks();
|
|
539
|
+
|
|
540
|
+
await processor.process(workflow, args, context);
|
|
541
|
+
|
|
542
|
+
expect(mockSearchCode.execute).toHaveBeenCalledWith(
|
|
543
|
+
expect.objectContaining({ query: 'repo:octocat/Hello-World' }),
|
|
544
|
+
expect.anything(),
|
|
545
|
+
expect.anything(),
|
|
546
|
+
expect.anything(),
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('workflow execution — auth flow', () => {
|
|
552
|
+
it('should stop at awaiting_auth when unauthorized', async () => {
|
|
553
|
+
const context = {} as RunContext;
|
|
554
|
+
|
|
555
|
+
mockGetAuthenticatedUser.execute.mockResolvedValue({
|
|
556
|
+
data: {
|
|
557
|
+
error: 'unauthorized',
|
|
558
|
+
message: 'No valid GitHub token found. Please authenticate first.',
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
mockTask.execute.mockResolvedValue({
|
|
563
|
+
data: { pipelineId: 'test-pipeline-id' },
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const result = await processor.process(workflow, { owner: 'octocat', repo: 'Hello-World' }, context);
|
|
567
|
+
|
|
568
|
+
expect(result).toBeDefined();
|
|
569
|
+
expect(result.hasError).toBe(false);
|
|
570
|
+
expect(result.stop).toBe(true);
|
|
571
|
+
expect(result.place).toBe('awaiting_auth');
|
|
572
|
+
|
|
573
|
+
expect(mockGetAuthenticatedUser.execute).toHaveBeenCalledTimes(1);
|
|
574
|
+
expect(mockTask.execute).toHaveBeenCalledTimes(1);
|
|
575
|
+
expect(mockTask.execute).toHaveBeenCalledWith(
|
|
576
|
+
expect.objectContaining({
|
|
577
|
+
workflow: 'oAuth',
|
|
578
|
+
args: expect.objectContaining({
|
|
579
|
+
provider: 'github',
|
|
580
|
+
scopes: ['repo', 'read:org', 'workflow'],
|
|
581
|
+
}),
|
|
582
|
+
callback: { transition: 'auth_completed' },
|
|
583
|
+
}),
|
|
584
|
+
expect.anything(),
|
|
585
|
+
expect.anything(),
|
|
586
|
+
expect.anything(),
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Subsequent tools should NOT be called
|
|
590
|
+
expect(mockListUserOrgs.execute).not.toHaveBeenCalled();
|
|
591
|
+
expect(mockGetRepo.execute).not.toHaveBeenCalled();
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe('GitHubReposOverviewWorkflow with existing entity', () => {
|
|
597
|
+
let module: TestingModule;
|
|
598
|
+
|
|
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
|
+
afterEach(async () => {
|
|
611
|
+
if (module) {
|
|
612
|
+
await module.close();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('should resume from awaiting_auth and complete after auth_completed', async () => {
|
|
617
|
+
const workflowId = '00000000-0000-0000-0000-000000000001';
|
|
618
|
+
const args = { owner: 'octocat', repo: 'Hello-World' };
|
|
619
|
+
|
|
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();
|
|
630
|
+
|
|
631
|
+
const workflow = module.get(GitHubReposOverviewWorkflow);
|
|
632
|
+
const processor = module.get(WorkflowProcessorService);
|
|
633
|
+
const mockGetAuthenticatedUser: ToolMock = module.get(GitHubGetAuthenticatedUserTool);
|
|
634
|
+
const mockListUserOrgs: ToolMock = module.get(GitHubListUserOrgsTool);
|
|
635
|
+
const mockGetRepo: ToolMock = module.get(GitHubGetRepoTool);
|
|
636
|
+
const mockListBranches: ToolMock = module.get(GitHubListBranchesTool);
|
|
637
|
+
const mockListIssues: ToolMock = module.get(GitHubListIssuesTool);
|
|
638
|
+
const mockListPullRequests: ToolMock = module.get(GitHubListPullRequestsTool);
|
|
639
|
+
const mockListDirectory: ToolMock = module.get(GitHubListDirectoryTool);
|
|
640
|
+
const mockListWorkflowRuns: ToolMock = module.get(GitHubListWorkflowRunsTool);
|
|
641
|
+
const mockSearchCode: ToolMock = module.get(GitHubSearchCodeTool);
|
|
642
|
+
const mockCreateDocument: ToolMock = module.get(CreateDocument);
|
|
643
|
+
|
|
644
|
+
// After auth, the workflow retries from start and succeeds
|
|
645
|
+
mockGetAuthenticatedUser.execute.mockResolvedValue({
|
|
646
|
+
data: {
|
|
647
|
+
user: {
|
|
648
|
+
id: 1,
|
|
649
|
+
login: 'octocat',
|
|
650
|
+
name: 'The Octocat',
|
|
651
|
+
email: null,
|
|
652
|
+
avatarUrl: '',
|
|
653
|
+
htmlUrl: 'https://github.com/octocat',
|
|
654
|
+
bio: null,
|
|
655
|
+
publicRepos: 8,
|
|
656
|
+
followers: 100,
|
|
657
|
+
following: 0,
|
|
658
|
+
createdAt: '2011-01-25T00:00:00Z',
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
mockListUserOrgs.execute.mockResolvedValue({ data: { orgs: [] } });
|
|
663
|
+
mockGetRepo.execute.mockResolvedValue({
|
|
664
|
+
data: {
|
|
665
|
+
repo: {
|
|
666
|
+
id: 1,
|
|
667
|
+
fullName: 'octocat/Hello-World',
|
|
668
|
+
name: 'Hello-World',
|
|
669
|
+
owner: 'octocat',
|
|
670
|
+
ownerAvatar: '',
|
|
671
|
+
private: false,
|
|
672
|
+
htmlUrl: 'https://github.com/octocat/Hello-World',
|
|
673
|
+
description: 'Test',
|
|
674
|
+
language: 'JS',
|
|
675
|
+
defaultBranch: 'main',
|
|
676
|
+
stars: 1,
|
|
677
|
+
forks: 0,
|
|
678
|
+
openIssues: 0,
|
|
679
|
+
createdAt: '',
|
|
680
|
+
updatedAt: '',
|
|
681
|
+
topics: [],
|
|
682
|
+
license: null,
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
});
|
|
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: [] } });
|
|
692
|
+
|
|
693
|
+
const context = {
|
|
694
|
+
payload: {
|
|
695
|
+
transition: {
|
|
696
|
+
id: 'auth_completed',
|
|
697
|
+
workflowId,
|
|
698
|
+
payload: { pipelineId: 'auth-pipeline-id' },
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
} as unknown as RunContext;
|
|
702
|
+
|
|
703
|
+
const result = await processor.process(workflow, args, context);
|
|
704
|
+
|
|
705
|
+
expect(result).toBeDefined();
|
|
706
|
+
expect(result.hasError).toBe(false);
|
|
707
|
+
expect(result.place).toBe('end');
|
|
708
|
+
|
|
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();
|
|
721
|
+
|
|
722
|
+
const workflow = module.get(GitHubReposOverviewWorkflow);
|
|
723
|
+
expect(workflow).toBeDefined();
|
|
724
|
+
});
|
|
725
|
+
});
|