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.
- package/.github/workflows/umino-project.yml +1 -0
- package/CHANGELOG.md +20 -0
- package/README.md +24 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +101 -33
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/FetchWebhookRepository.js +10 -0
- package/bin/adapter/repositories/FetchWebhookRepository.js.map +1 -0
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js +190 -0
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js.map +1 -0
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +34 -0
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -0
- package/bin/adapter/repositories/StubClaudeRepository.js +13 -0
- package/bin/adapter/repositories/StubClaudeRepository.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +29 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +73 -17
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +23 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +29 -2
- package/src/adapter/repositories/FetchWebhookRepository.ts +7 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.ts +291 -0
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +80 -0
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +37 -0
- package/src/adapter/repositories/StubClaudeRepository.test.ts +19 -0
- package/src/adapter/repositories/StubClaudeRepository.ts +12 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +31 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +51 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +722 -16
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +117 -20
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -0
- package/src/domain/usecases/adapter-interfaces/WebhookRepository.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/FetchWebhookRepository.d.ts +5 -0
- package/types/adapter/repositories/FetchWebhookRepository.d.ts.map +1 -0
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts +12 -0
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts.map +1 -0
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +9 -0
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -0
- package/types/adapter/repositories/StubClaudeRepository.d.ts +7 -0
- package/types/adapter/repositories/StubClaudeRepository.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +19 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts +4 -0
- 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[] = [];
|