github-issue-tower-defence-management 1.31.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.github/workflows/umino-project.yml +1 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +24 -0
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +101 -33
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  6. package/bin/adapter/repositories/FetchWebhookRepository.js +10 -0
  7. package/bin/adapter/repositories/FetchWebhookRepository.js.map +1 -0
  8. package/bin/adapter/repositories/GitHubIssueCommentRepository.js +190 -0
  9. package/bin/adapter/repositories/GitHubIssueCommentRepository.js.map +1 -0
  10. package/bin/adapter/repositories/NodeLocalCommandRunner.js +34 -0
  11. package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -0
  12. package/bin/adapter/repositories/StubClaudeRepository.js +13 -0
  13. package/bin/adapter/repositories/StubClaudeRepository.js.map +1 -0
  14. package/bin/domain/usecases/HandleScheduledEventUseCase.js +29 -1
  15. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  16. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +73 -17
  17. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  18. package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js +3 -0
  19. package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js.map +1 -0
  20. package/package.json +1 -1
  21. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +23 -0
  22. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +29 -2
  23. package/src/adapter/repositories/FetchWebhookRepository.ts +7 -0
  24. package/src/adapter/repositories/GitHubIssueCommentRepository.ts +291 -0
  25. package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +80 -0
  26. package/src/adapter/repositories/NodeLocalCommandRunner.ts +37 -0
  27. package/src/adapter/repositories/StubClaudeRepository.test.ts +19 -0
  28. package/src/adapter/repositories/StubClaudeRepository.ts +12 -0
  29. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +31 -0
  30. package/src/domain/usecases/HandleScheduledEventUseCase.ts +51 -0
  31. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +722 -16
  32. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +117 -20
  33. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -0
  34. package/src/domain/usecases/adapter-interfaces/WebhookRepository.ts +3 -0
  35. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  36. package/types/adapter/repositories/FetchWebhookRepository.d.ts +5 -0
  37. package/types/adapter/repositories/FetchWebhookRepository.d.ts.map +1 -0
  38. package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts +12 -0
  39. package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts.map +1 -0
  40. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +9 -0
  41. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -0
  42. package/types/adapter/repositories/StubClaudeRepository.d.ts +7 -0
  43. package/types/adapter/repositories/StubClaudeRepository.d.ts.map +1 -0
  44. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +19 -1
  45. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  46. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
  47. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  48. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -0
  49. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  50. package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts +4 -0
  51. package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts.map +1 -0
@@ -0,0 +1,80 @@
1
+ const mockExecAsync = jest.fn();
2
+
3
+ jest.mock('child_process', () => ({
4
+ exec: jest.fn(),
5
+ }));
6
+
7
+ jest.mock('util', () => ({
8
+ promisify: jest.fn(() => mockExecAsync),
9
+ }));
10
+
11
+ import { NodeLocalCommandRunner } from './NodeLocalCommandRunner';
12
+
13
+ describe('NodeLocalCommandRunner', () => {
14
+ let runner: NodeLocalCommandRunner;
15
+
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ runner = new NodeLocalCommandRunner();
19
+ });
20
+
21
+ describe('runCommand', () => {
22
+ it('should execute command successfully', async () => {
23
+ mockExecAsync.mockResolvedValue({
24
+ stdout: 'command output',
25
+ stderr: '',
26
+ });
27
+
28
+ const result = await runner.runCommand('echo "test"');
29
+
30
+ expect(result).toEqual({
31
+ stdout: 'command output',
32
+ stderr: '',
33
+ exitCode: 0,
34
+ });
35
+ expect(mockExecAsync).toHaveBeenCalledWith('echo "test"');
36
+ });
37
+
38
+ it('should handle command errors with exit code', async () => {
39
+ const error = Object.assign(new Error('Command failed'), {
40
+ code: 2,
41
+ stdout: 'partial output',
42
+ stderr: 'error message',
43
+ });
44
+ mockExecAsync.mockRejectedValue(error);
45
+
46
+ const result = await runner.runCommand('invalid-command');
47
+
48
+ expect(result).toEqual({
49
+ stdout: 'partial output',
50
+ stderr: 'error message',
51
+ exitCode: 2,
52
+ });
53
+ });
54
+
55
+ it('should default to exit code 1 when code is not a number', async () => {
56
+ const error = Object.assign(new Error('Command failed'), {
57
+ code: 'ENOENT',
58
+ stdout: '',
59
+ stderr: 'command not found',
60
+ });
61
+ mockExecAsync.mockRejectedValue(error);
62
+
63
+ const result = await runner.runCommand('nonexistent');
64
+
65
+ expect(result).toEqual({
66
+ stdout: '',
67
+ stderr: 'command not found',
68
+ exitCode: 1,
69
+ });
70
+ });
71
+
72
+ it('should throw error if error format is unexpected', async () => {
73
+ mockExecAsync.mockRejectedValue(new Error('Unexpected error'));
74
+
75
+ await expect(runner.runCommand('command')).rejects.toThrow(
76
+ 'Unexpected error',
77
+ );
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,37 @@
1
+ import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+
5
+ const execAsync = promisify(exec);
6
+
7
+ export class NodeLocalCommandRunner implements LocalCommandRunner {
8
+ async runCommand(command: string): Promise<{
9
+ stdout: string;
10
+ stderr: string;
11
+ exitCode: number;
12
+ }> {
13
+ try {
14
+ const { stdout, stderr } = await execAsync(command);
15
+ return {
16
+ stdout,
17
+ stderr,
18
+ exitCode: 0,
19
+ };
20
+ } catch (error) {
21
+ if (
22
+ error &&
23
+ typeof error === 'object' &&
24
+ 'stdout' in error &&
25
+ 'stderr' in error &&
26
+ 'code' in error
27
+ ) {
28
+ return {
29
+ stdout: String(error.stdout),
30
+ stderr: String(error.stderr),
31
+ exitCode: typeof error.code === 'number' ? error.code : 1,
32
+ };
33
+ }
34
+ throw error;
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,19 @@
1
+ import { StubClaudeRepository } from './StubClaudeRepository';
2
+
3
+ describe('StubClaudeRepository', () => {
4
+ let repository: StubClaudeRepository;
5
+
6
+ beforeEach(() => {
7
+ repository = new StubClaudeRepository();
8
+ });
9
+
10
+ it('should return empty usage array', async () => {
11
+ const result = await repository.getUsage();
12
+ expect(result).toEqual([]);
13
+ });
14
+
15
+ it('should return true for isClaudeAvailable', async () => {
16
+ const result = await repository.isClaudeAvailable(90);
17
+ expect(result).toBe(true);
18
+ });
19
+ });
@@ -0,0 +1,12 @@
1
+ import { ClaudeRepository } from '../../domain/usecases/adapter-interfaces/ClaudeRepository';
2
+ import { ClaudeWindowUsage } from '../../domain/entities/ClaudeWindowUsage';
3
+
4
+ export class StubClaudeRepository implements ClaudeRepository {
5
+ async getUsage(): Promise<ClaudeWindowUsage[]> {
6
+ return [];
7
+ }
8
+
9
+ async isClaudeAvailable(_threshold: number): Promise<boolean> {
10
+ return true;
11
+ }
12
+ }
@@ -18,6 +18,8 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
18
18
  import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
19
19
  import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
20
20
  import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
21
+ import { StartPreparationUseCase } from './StartPreparationUseCase';
22
+ import { NotifyFinishedIssuePreparationUseCase } from './NotifyFinishedIssuePreparationUseCase';
21
23
 
22
24
  describe('HandleScheduledEventUseCase', () => {
23
25
  describe('createTargetDateTimes', () => {
@@ -99,6 +101,9 @@ describe('HandleScheduledEventUseCase', () => {
99
101
  mock<AssignNoAssigneeIssueToManagerUseCase>();
100
102
  const mockUpdateIssueStatusByLabelUseCase =
101
103
  mock<UpdateIssueStatusByLabelUseCase>();
104
+ const mockStartPreparationUseCase = mock<StartPreparationUseCase>();
105
+ const mockNotifyFinishedIssuePreparationUseCase =
106
+ mock<NotifyFinishedIssuePreparationUseCase>();
102
107
  const mockDateRepository = mock<DateRepository>();
103
108
  const mockSpreadsheetRepository = mock<SpreadsheetRepository>();
104
109
  const mockProjectRepository = mock<ProjectRepository>();
@@ -118,6 +123,8 @@ describe('HandleScheduledEventUseCase', () => {
118
123
  mockCreateNewStoryByLabelUseCase,
119
124
  mockAssignNoAssigneeIssueToManagerUseCase,
120
125
  mockUpdateIssueStatusByLabelUseCase,
126
+ mockStartPreparationUseCase,
127
+ mockNotifyFinishedIssuePreparationUseCase,
121
128
  mockDateRepository,
122
129
  mockSpreadsheetRepository,
123
130
  mockProjectRepository,
@@ -141,6 +148,30 @@ describe('HandleScheduledEventUseCase', () => {
141
148
  ]);
142
149
  });
143
150
 
151
+ it('should call AnalyzeProblemByIssueUseCase with correct parameters', async () => {
152
+ const input = {
153
+ projectName: 'test-project',
154
+ org: 'test-org',
155
+ projectUrl: 'https://github.com/test-org/test-project',
156
+ manager: 'test-manager',
157
+ workingReport: {
158
+ repo: 'test-repo',
159
+ members: ['member1'],
160
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
161
+ },
162
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
163
+ disabledStatus: 'disabled',
164
+ defaultStatus: null,
165
+ disabled: false,
166
+ allowIssueCacheMinutes: 60,
167
+ };
168
+
169
+ const mockProject = mock<Project>();
170
+ mockProjectRepository.getProject.mockResolvedValue(mockProject);
171
+ await useCase.run(input);
172
+ expect(mockAnalyzeProblemByIssueUseCase.run).toHaveBeenCalled();
173
+ });
174
+
144
175
  it('should call UpdateIssueStatusByLabelUseCase with correct parameters', async () => {
145
176
  const input = {
146
177
  projectName: 'test-project',
@@ -19,6 +19,8 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
19
19
  import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
20
20
  import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
21
21
  import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
22
+ import { StartPreparationUseCase } from './StartPreparationUseCase';
23
+ import { NotifyFinishedIssuePreparationUseCase } from './NotifyFinishedIssuePreparationUseCase';
22
24
 
23
25
  export class ProjectNotFoundError extends Error {
24
26
  constructor(message: string) {
@@ -42,6 +44,8 @@ export class HandleScheduledEventUseCase {
42
44
  readonly createNewStoryByLabelUseCase: CreateNewStoryByLabelUseCase,
43
45
  readonly assignNoAssigneeIssueToManagerUseCase: AssignNoAssigneeIssueToManagerUseCase,
44
46
  readonly updateIssueStatusByLabelUseCase: UpdateIssueStatusByLabelUseCase,
47
+ readonly startPreparationUseCase: StartPreparationUseCase,
48
+ readonly notifyFinishedIssuePreparationUseCase: NotifyFinishedIssuePreparationUseCase,
45
49
  readonly dateRepository: DateRepository,
46
50
  readonly spreadsheetRepository: SpreadsheetRepository,
47
51
  readonly projectRepository: ProjectRepository,
@@ -63,6 +67,20 @@ export class HandleScheduledEventUseCase {
63
67
  defaultStatus: string | null;
64
68
  disabled: boolean;
65
69
  allowIssueCacheMinutes: number;
70
+ startPreparation?: {
71
+ awaitingWorkspaceStatus: string;
72
+ preparationStatus: string;
73
+ defaultAgentName: string;
74
+ logFilePath?: string;
75
+ maximumPreparingIssuesCount: number | null;
76
+ } | null;
77
+ notifyFinishedPreparation?: {
78
+ preparationStatus: string;
79
+ awaitingWorkspaceStatus: string;
80
+ awaitingQualityCheckStatus: string;
81
+ thresholdForAutoReject: number;
82
+ workflowBlockerResolvedWebhookUrl: string | null;
83
+ } | null;
66
84
  }): Promise<{
67
85
  project: Project;
68
86
  issues: Issue[];
@@ -292,6 +310,39 @@ ${JSON.stringify(e)}
292
310
  issues,
293
311
  defaultStatus: input.defaultStatus,
294
312
  });
313
+ if (input.startPreparation) {
314
+ await this.startPreparationUseCase.run({
315
+ projectUrl: input.projectUrl,
316
+ awaitingWorkspaceStatus: input.startPreparation.awaitingWorkspaceStatus,
317
+ preparationStatus: input.startPreparation.preparationStatus,
318
+ defaultAgentName: input.startPreparation.defaultAgentName,
319
+ logFilePath: input.startPreparation.logFilePath,
320
+ maximumPreparingIssuesCount:
321
+ input.startPreparation.maximumPreparingIssuesCount,
322
+ allowIssueCacheMinutes: input.allowIssueCacheMinutes,
323
+ });
324
+ }
325
+ if (input.notifyFinishedPreparation) {
326
+ const notifyFinishedPreparation = input.notifyFinishedPreparation;
327
+ const preparationIssues = issues.filter(
328
+ (issue) => issue.status === notifyFinishedPreparation.preparationStatus,
329
+ );
330
+ for (const issue of preparationIssues) {
331
+ await this.notifyFinishedIssuePreparationUseCase.run({
332
+ projectUrl: input.projectUrl,
333
+ issueUrl: issue.url,
334
+ preparationStatus: input.notifyFinishedPreparation.preparationStatus,
335
+ awaitingWorkspaceStatus:
336
+ input.notifyFinishedPreparation.awaitingWorkspaceStatus,
337
+ awaitingQualityCheckStatus:
338
+ input.notifyFinishedPreparation.awaitingQualityCheckStatus,
339
+ thresholdForAutoReject:
340
+ input.notifyFinishedPreparation.thresholdForAutoReject,
341
+ workflowBlockerResolvedWebhookUrl:
342
+ input.notifyFinishedPreparation.workflowBlockerResolvedWebhookUrl,
343
+ });
344
+ }
345
+ }
295
346
  };
296
347
  static createTargetDateTimes = (from: Date, to: Date): Date[] => {
297
348
  const targetDateTimes: Date[] = [];