github-issue-tower-defence-management 1.164.0 → 1.165.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/CHANGELOG.md +19 -0
- package/bin/adapter/entry-points/cli/index.js +1 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +1 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +40 -3
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +56 -5
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/projectFieldDefinition.js +8 -0
- package/bin/adapter/repositories/projectFieldDefinition.js.map +1 -1
- package/bin/domain/entities/RequiredProjectField.js +2 -1
- package/bin/domain/entities/RequiredProjectField.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +73 -44
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +40 -2
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +5 -4
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -0
- package/src/adapter/entry-points/cli/index.ts +1 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +1 -0
- package/src/adapter/entry-points/console/consoleTabNames.test.ts +1 -0
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +2 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +2 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
- package/src/adapter/repositories/GraphqlProjectRepository.diskCache.test.ts +1 -0
- package/src/adapter/repositories/GraphqlProjectRepository.test.ts +2 -0
- package/src/adapter/repositories/GraphqlProjectRepository.ts +53 -3
- package/src/adapter/repositories/GraphqlProjectRepositoryProjectLocation.test.ts +1 -0
- package/src/adapter/repositories/ProjectIssuesCacheRepository.test.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +156 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +91 -6
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/adapter/repositories/projectFieldDefinition.test.ts +1 -0
- package/src/adapter/repositories/projectFieldDefinition.ts +12 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/entities/Project.ts +5 -0
- package/src/domain/entities/RequiredProjectField.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +2 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +156 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +120 -64
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +86 -1
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +62 -7
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +2 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +2 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +2 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +2 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +2 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
- package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +2 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +2 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts +3 -0
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/projectFieldDefinition.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +5 -0
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/entities/RequiredProjectField.d.ts +1 -0
- package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +4 -2
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +5 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
|
@@ -3,6 +3,7 @@ import { Issue } from '../entities/Issue';
|
|
|
3
3
|
import { Project } from '../entities/Project';
|
|
4
4
|
import { Comment } from '../entities/Comment';
|
|
5
5
|
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
6
|
+
import { AGENT_FIELD_NAME } from '../entities/RequiredProjectField';
|
|
6
7
|
|
|
7
8
|
const createMockProject = (overrides: Partial<Project> = {}): Project => ({
|
|
8
9
|
id: 'project-1',
|
|
@@ -45,6 +46,7 @@ const createMockProject = (overrides: Partial<Project> = {}): Project => ({
|
|
|
45
46
|
remainingEstimationMinutes: null,
|
|
46
47
|
dependedIssueUrlSeparatedByComma: null,
|
|
47
48
|
completionDate50PercentConfidence: null,
|
|
49
|
+
agent: null,
|
|
48
50
|
...overrides,
|
|
49
51
|
});
|
|
50
52
|
|
|
@@ -73,6 +75,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
|
73
75
|
createdAt: new Date(),
|
|
74
76
|
author: '',
|
|
75
77
|
closingIssueReferenceUrls: [],
|
|
78
|
+
agent: null,
|
|
76
79
|
...overrides,
|
|
77
80
|
});
|
|
78
81
|
|
|
@@ -87,6 +90,8 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
87
90
|
let useCase: NotifyFinishedIssuePreparationUseCase;
|
|
88
91
|
let mockProjectRepository: {
|
|
89
92
|
getByUrl: jest.Mock;
|
|
93
|
+
updateAgentList: jest.Mock;
|
|
94
|
+
createField: jest.Mock;
|
|
90
95
|
};
|
|
91
96
|
let mockIssueRepository: {
|
|
92
97
|
get: jest.Mock;
|
|
@@ -101,6 +106,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
101
106
|
approvePullRequest: jest.Mock;
|
|
102
107
|
requestChangesWithInlineComment: jest.Mock;
|
|
103
108
|
setDependedIssueUrl: jest.Mock;
|
|
109
|
+
setIssueAgentField: jest.Mock;
|
|
104
110
|
};
|
|
105
111
|
let mockIssueCommentRepository: {
|
|
106
112
|
getCommentsFromIssue: jest.Mock;
|
|
@@ -123,6 +129,8 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
123
129
|
|
|
124
130
|
mockProjectRepository = {
|
|
125
131
|
getByUrl: jest.fn(),
|
|
132
|
+
updateAgentList: jest.fn().mockResolvedValue([]),
|
|
133
|
+
createField: jest.fn().mockResolvedValue(undefined),
|
|
126
134
|
};
|
|
127
135
|
|
|
128
136
|
mockIssueRepository = {
|
|
@@ -138,6 +146,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
138
146
|
approvePullRequest: jest.fn().mockResolvedValue(undefined),
|
|
139
147
|
requestChangesWithInlineComment: jest.fn().mockResolvedValue(undefined),
|
|
140
148
|
setDependedIssueUrl: jest.fn(),
|
|
149
|
+
setIssueAgentField: jest.fn().mockResolvedValue(undefined),
|
|
141
150
|
};
|
|
142
151
|
|
|
143
152
|
mockIssueCommentRepository = {
|
|
@@ -3599,6 +3608,153 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
3599
3608
|
});
|
|
3600
3609
|
});
|
|
3601
3610
|
|
|
3611
|
+
describe('nextStepAgent handling', () => {
|
|
3612
|
+
const makeProjectWithAgent = () =>
|
|
3613
|
+
createMockProject({
|
|
3614
|
+
agent: {
|
|
3615
|
+
name: 'Agent',
|
|
3616
|
+
fieldId: 'agent-field-id',
|
|
3617
|
+
options: [
|
|
3618
|
+
{ id: 'opt-impl', name: 'impl', color: 'GRAY', description: '' },
|
|
3619
|
+
],
|
|
3620
|
+
},
|
|
3621
|
+
});
|
|
3622
|
+
|
|
3623
|
+
it('calls setIssueAgentField with existing option ID when nextStepAgent is in last comment', async () => {
|
|
3624
|
+
const issue = createMockIssue({ status: 'Preparation' });
|
|
3625
|
+
const projectWithAgent = makeProjectWithAgent();
|
|
3626
|
+
mockProjectRepository.getByUrl.mockResolvedValue(projectWithAgent);
|
|
3627
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3628
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
3629
|
+
createMockComment({
|
|
3630
|
+
content:
|
|
3631
|
+
'From: :robot: agent (model)\n```json\n{"nextStep": "do something", "nextStepAgent": "impl"}\n```',
|
|
3632
|
+
}),
|
|
3633
|
+
]);
|
|
3634
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
3635
|
+
|
|
3636
|
+
await useCase.run({
|
|
3637
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3638
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3639
|
+
thresholdForAutoReject: 3,
|
|
3640
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3641
|
+
allowedIssueAuthors: null,
|
|
3642
|
+
});
|
|
3643
|
+
|
|
3644
|
+
expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledWith(
|
|
3645
|
+
'https://github.com/user/repo/issues/1',
|
|
3646
|
+
projectWithAgent,
|
|
3647
|
+
'opt-impl',
|
|
3648
|
+
);
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
it('creates a new agent option and calls setIssueAgentField when nextStepAgent is not an existing option', async () => {
|
|
3652
|
+
const issue = createMockIssue({ status: 'Preparation' });
|
|
3653
|
+
const projectWithAgent = makeProjectWithAgent();
|
|
3654
|
+
mockProjectRepository.getByUrl.mockResolvedValue(projectWithAgent);
|
|
3655
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3656
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
3657
|
+
createMockComment({
|
|
3658
|
+
content:
|
|
3659
|
+
'From: :robot: agent (model)\n```json\n{"nextStep": "review", "nextStepAgent": "chore"}\n```',
|
|
3660
|
+
}),
|
|
3661
|
+
]);
|
|
3662
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
3663
|
+
mockProjectRepository.updateAgentList.mockResolvedValue([
|
|
3664
|
+
{ id: 'opt-impl', name: 'impl', color: 'GRAY', description: '' },
|
|
3665
|
+
{ id: 'opt-chore-new', name: 'chore', color: 'GRAY', description: '' },
|
|
3666
|
+
]);
|
|
3667
|
+
|
|
3668
|
+
await useCase.run({
|
|
3669
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3670
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3671
|
+
thresholdForAutoReject: 3,
|
|
3672
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3673
|
+
allowedIssueAuthors: null,
|
|
3674
|
+
});
|
|
3675
|
+
|
|
3676
|
+
expect(mockProjectRepository.updateAgentList).toHaveBeenCalledWith(
|
|
3677
|
+
projectWithAgent,
|
|
3678
|
+
expect.arrayContaining([
|
|
3679
|
+
expect.objectContaining({ name: 'impl' }),
|
|
3680
|
+
expect.objectContaining({ id: null, name: 'chore', color: 'GRAY' }),
|
|
3681
|
+
]),
|
|
3682
|
+
);
|
|
3683
|
+
expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledWith(
|
|
3684
|
+
'https://github.com/user/repo/issues/1',
|
|
3685
|
+
projectWithAgent,
|
|
3686
|
+
'opt-chore-new',
|
|
3687
|
+
);
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
it('does not call setIssueAgentField when nextStepAgent is absent from the last comment', async () => {
|
|
3691
|
+
const issue = createMockIssue({ status: 'Preparation' });
|
|
3692
|
+
const projectWithAgent = makeProjectWithAgent();
|
|
3693
|
+
mockProjectRepository.getByUrl.mockResolvedValue(projectWithAgent);
|
|
3694
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3695
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
3696
|
+
createMockComment({
|
|
3697
|
+
content:
|
|
3698
|
+
'From: :robot: agent (model)\n```json\n{"nextStep": null}\n```',
|
|
3699
|
+
}),
|
|
3700
|
+
]);
|
|
3701
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
3702
|
+
{
|
|
3703
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
3704
|
+
isConflicted: false,
|
|
3705
|
+
isPassedAllCiJob: true,
|
|
3706
|
+
isCiStateSuccess: true,
|
|
3707
|
+
isResolvedAllReviewComments: true,
|
|
3708
|
+
isBranchOutOfDate: false,
|
|
3709
|
+
missingRequiredCheckNames: [],
|
|
3710
|
+
},
|
|
3711
|
+
]);
|
|
3712
|
+
|
|
3713
|
+
await useCase.run({
|
|
3714
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3715
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3716
|
+
thresholdForAutoReject: 3,
|
|
3717
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3718
|
+
allowedIssueAuthors: null,
|
|
3719
|
+
});
|
|
3720
|
+
|
|
3721
|
+
expect(mockIssueRepository.setIssueAgentField).not.toHaveBeenCalled();
|
|
3722
|
+
expect(mockProjectRepository.updateAgentList).not.toHaveBeenCalled();
|
|
3723
|
+
});
|
|
3724
|
+
|
|
3725
|
+
it('creates agent field via createField and does not call setIssueAgentField when project has no agent field and re-fetch also returns no agent', async () => {
|
|
3726
|
+
const issue = createMockIssue({ status: 'Preparation' });
|
|
3727
|
+
const projectWithoutAgent = createMockProject({ agent: null });
|
|
3728
|
+
mockProjectRepository.getByUrl.mockResolvedValue(projectWithoutAgent);
|
|
3729
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3730
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
3731
|
+
createMockComment({
|
|
3732
|
+
content:
|
|
3733
|
+
'From: :robot: agent (model)\n```json\n{"nextStep": "go", "nextStepAgent": "impl"}\n```',
|
|
3734
|
+
}),
|
|
3735
|
+
]);
|
|
3736
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
3737
|
+
|
|
3738
|
+
await useCase.run({
|
|
3739
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3740
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3741
|
+
thresholdForAutoReject: 3,
|
|
3742
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3743
|
+
allowedIssueAuthors: null,
|
|
3744
|
+
});
|
|
3745
|
+
|
|
3746
|
+
expect(mockProjectRepository.createField).toHaveBeenCalledWith(
|
|
3747
|
+
projectWithoutAgent,
|
|
3748
|
+
{
|
|
3749
|
+
name: AGENT_FIELD_NAME,
|
|
3750
|
+
dataType: 'SINGLE_SELECT',
|
|
3751
|
+
options: [{ name: 'impl', color: 'GRAY', description: '' }],
|
|
3752
|
+
},
|
|
3753
|
+
);
|
|
3754
|
+
expect(mockIssueRepository.setIssueAgentField).not.toHaveBeenCalled();
|
|
3755
|
+
});
|
|
3756
|
+
});
|
|
3757
|
+
|
|
3602
3758
|
describe('consoleTabsRepository integration', () => {
|
|
3603
3759
|
let mockConsoleTabsRepository: {
|
|
3604
3760
|
patchIssueTabTransition: jest.Mock;
|
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
ConsoleTabName,
|
|
26
26
|
} from './console/GenerateConsoleListsUseCase';
|
|
27
27
|
import { Issue } from '../entities/Issue';
|
|
28
|
+
import { FieldOption, Project } from '../entities/Project';
|
|
29
|
+
import { normalizeProjectFieldName } from '../entities/ProjectFieldName';
|
|
30
|
+
import { AGENT_FIELD_NAME } from '../entities/RequiredProjectField';
|
|
28
31
|
|
|
29
32
|
export class IssueNotFoundError extends Error {
|
|
30
33
|
constructor(issueUrl: string) {
|
|
@@ -52,7 +55,10 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
52
55
|
private readonly changeTargetPullRequestApprover: ChangeTargetPullRequestApprover;
|
|
53
56
|
|
|
54
57
|
constructor(
|
|
55
|
-
private readonly projectRepository: Pick<
|
|
58
|
+
private readonly projectRepository: Pick<
|
|
59
|
+
ProjectRepository,
|
|
60
|
+
'getByUrl' | 'updateAgentList' | 'createField'
|
|
61
|
+
>,
|
|
56
62
|
private readonly issueRepository: Pick<
|
|
57
63
|
IssueRepository,
|
|
58
64
|
| 'get'
|
|
@@ -67,6 +73,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
67
73
|
| 'approvePullRequest'
|
|
68
74
|
| 'requestChangesWithInlineComment'
|
|
69
75
|
| 'setDependedIssueUrl'
|
|
76
|
+
| 'setIssueAgentField'
|
|
70
77
|
>,
|
|
71
78
|
private readonly issueCommentRepository: Pick<
|
|
72
79
|
IssueCommentRepository,
|
|
@@ -93,6 +100,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
93
100
|
labelsAsLlmAgentName?: string[] | null;
|
|
94
101
|
labelsNotRequiringPullRequest?: string[] | null;
|
|
95
102
|
changeTargetPathAliases?: Record<string, string> | null;
|
|
103
|
+
agents?: string[] | null;
|
|
96
104
|
}): Promise<void> => {
|
|
97
105
|
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
98
106
|
|
|
@@ -195,41 +203,53 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
195
203
|
const isTrustedAuthor = (author: string): boolean =>
|
|
196
204
|
this.isAuthorTrusted(author, params.allowedIssueAuthors ?? null);
|
|
197
205
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
const updatedLabels = [...new Set([...filteredLabels, nextStepAgent])];
|
|
217
|
-
issue.labels = updatedLabels;
|
|
218
|
-
issue.status = AWAITING_WORKSPACE_STATUS_NAME;
|
|
219
|
-
await this.issueRepository.update(issue, project);
|
|
220
|
-
await this.issueRepository.updateStatus(
|
|
206
|
+
const lastTrustedBotComment = [...comments]
|
|
207
|
+
.reverse()
|
|
208
|
+
.find(
|
|
209
|
+
(c) =>
|
|
210
|
+
isTrustedAuthor(c.author) && c.content.startsWith('From: :robot:'),
|
|
211
|
+
);
|
|
212
|
+
const nextStepAgent = lastTrustedBotComment
|
|
213
|
+
? this.extractNextStepAgent(lastTrustedBotComment.content)
|
|
214
|
+
: null;
|
|
215
|
+
if (nextStepAgent !== null) {
|
|
216
|
+
const agentOptionId = await this.ensureAgentOptionAndGetId(
|
|
217
|
+
project,
|
|
218
|
+
nextStepAgent,
|
|
219
|
+
);
|
|
220
|
+
if (agentOptionId) {
|
|
221
|
+
await this.issueRepository.setIssueAgentField(
|
|
222
|
+
params.issueUrl,
|
|
221
223
|
project,
|
|
222
|
-
|
|
223
|
-
awaitingWorkspaceStatusOption.id,
|
|
224
|
+
agentOptionId,
|
|
224
225
|
);
|
|
225
|
-
await this.issueRepository.updateLabels(issue, updatedLabels);
|
|
226
|
-
await this.patchConsoleTab(issue);
|
|
227
|
-
await this.issueCommentRepository.createComment(
|
|
228
|
-
issue,
|
|
229
|
-
`Next step agent: ${nextStepAgent}`,
|
|
230
|
-
);
|
|
231
|
-
return;
|
|
232
226
|
}
|
|
227
|
+
await this.issueRepository.getOrCreateLabel(
|
|
228
|
+
issue.org,
|
|
229
|
+
issue.repo,
|
|
230
|
+
nextStepAgent,
|
|
231
|
+
);
|
|
232
|
+
const agentLabels = params.labelsAsLlmAgentName ?? [];
|
|
233
|
+
const filteredLabels = issue.labels.filter(
|
|
234
|
+
(label) =>
|
|
235
|
+
!label.startsWith('llm-agent:') && !agentLabels.includes(label),
|
|
236
|
+
);
|
|
237
|
+
const updatedLabels = [...new Set([...filteredLabels, nextStepAgent])];
|
|
238
|
+
issue.labels = updatedLabels;
|
|
239
|
+
issue.status = AWAITING_WORKSPACE_STATUS_NAME;
|
|
240
|
+
await this.issueRepository.update(issue, project);
|
|
241
|
+
await this.issueRepository.updateStatus(
|
|
242
|
+
project,
|
|
243
|
+
issue,
|
|
244
|
+
awaitingWorkspaceStatusOption.id,
|
|
245
|
+
);
|
|
246
|
+
await this.issueRepository.updateLabels(issue, updatedLabels);
|
|
247
|
+
await this.patchConsoleTab(issue);
|
|
248
|
+
await this.issueCommentRepository.createComment(
|
|
249
|
+
issue,
|
|
250
|
+
`Next step agent: ${nextStepAgent}`,
|
|
251
|
+
);
|
|
252
|
+
return;
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
const { rejections, approvedPrUrl } = await this.collectRejections(
|
|
@@ -444,37 +464,6 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
444
464
|
return nextStepValue !== null && nextStepValue !== undefined;
|
|
445
465
|
};
|
|
446
466
|
|
|
447
|
-
private extractNextStepAgent = (body: string): string | null => {
|
|
448
|
-
const reportMatch = body.match(/```json\n([\s\S]*?)\n```/);
|
|
449
|
-
if (!reportMatch || reportMatch.length < 2) {
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
let reportJson: unknown;
|
|
453
|
-
try {
|
|
454
|
-
reportJson = JSON.parse(reportMatch[1]);
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.warn(
|
|
457
|
-
'Invalid JSON in report body while checking nextStepAgent:',
|
|
458
|
-
error,
|
|
459
|
-
);
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
if (typeof reportJson !== 'object' || reportJson === null) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
if (!('nextStepAgent' in reportJson)) {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
const nextStepAgentValue = Reflect.get(reportJson, 'nextStepAgent');
|
|
469
|
-
if (
|
|
470
|
-
typeof nextStepAgentValue !== 'string' ||
|
|
471
|
-
nextStepAgentValue.trim() === ''
|
|
472
|
-
) {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
return nextStepAgentValue;
|
|
476
|
-
};
|
|
477
|
-
|
|
478
467
|
private setDependedIssueUrlForAllOpenPRs = async (
|
|
479
468
|
issue: { url: string; labels: string[]; isPr: boolean },
|
|
480
469
|
issueUrl: string,
|
|
@@ -550,6 +539,73 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
550
539
|
return null;
|
|
551
540
|
};
|
|
552
541
|
|
|
542
|
+
private extractNextStepAgent = (body: string): string | null => {
|
|
543
|
+
const reportMatch = body.match(/```json\n([\s\S]*?)\n```/);
|
|
544
|
+
if (!reportMatch || reportMatch.length < 2) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
let reportJson: unknown;
|
|
548
|
+
try {
|
|
549
|
+
reportJson = JSON.parse(reportMatch[1]);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
console.warn(
|
|
552
|
+
'Invalid JSON in report body while checking nextStepAgent:',
|
|
553
|
+
error,
|
|
554
|
+
);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
if (typeof reportJson !== 'object' || reportJson === null) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
if (!('nextStepAgent' in reportJson)) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const value = Reflect.get(reportJson, 'nextStepAgent');
|
|
564
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
return value.trim();
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
private ensureAgentOptionAndGetId = async (
|
|
571
|
+
project: Project,
|
|
572
|
+
agentName: string,
|
|
573
|
+
): Promise<string | null> => {
|
|
574
|
+
const normalizedTarget = normalizeProjectFieldName(agentName);
|
|
575
|
+
if (!project.agent) {
|
|
576
|
+
await this.projectRepository.createField(project, {
|
|
577
|
+
name: AGENT_FIELD_NAME,
|
|
578
|
+
dataType: 'SINGLE_SELECT',
|
|
579
|
+
options: [{ name: agentName, color: 'GRAY' as const, description: '' }],
|
|
580
|
+
});
|
|
581
|
+
const refreshed = await this.projectRepository.getByUrl(project.url);
|
|
582
|
+
const created = refreshed.agent?.options.find(
|
|
583
|
+
(o) => normalizeProjectFieldName(o.name) === normalizedTarget,
|
|
584
|
+
);
|
|
585
|
+
return created?.id ?? null;
|
|
586
|
+
}
|
|
587
|
+
const existing = project.agent.options.find(
|
|
588
|
+
(o) => normalizeProjectFieldName(o.name) === normalizedTarget,
|
|
589
|
+
);
|
|
590
|
+
if (existing) {
|
|
591
|
+
return existing.id;
|
|
592
|
+
}
|
|
593
|
+
const mergedOptions: (Omit<FieldOption, 'id'> & {
|
|
594
|
+
id: FieldOption['id'] | null;
|
|
595
|
+
})[] = [
|
|
596
|
+
...project.agent.options.map((o) => ({ ...o })),
|
|
597
|
+
{ id: null, name: agentName, color: 'GRAY' as const, description: '' },
|
|
598
|
+
];
|
|
599
|
+
const updatedOptions = await this.projectRepository.updateAgentList(
|
|
600
|
+
project,
|
|
601
|
+
mergedOptions,
|
|
602
|
+
);
|
|
603
|
+
const added = updatedOptions.find(
|
|
604
|
+
(o) => normalizeProjectFieldName(o.name) === normalizedTarget,
|
|
605
|
+
);
|
|
606
|
+
return added?.id ?? null;
|
|
607
|
+
};
|
|
608
|
+
|
|
553
609
|
private patchConsoleTab = async (issue: Issue): Promise<void> => {
|
|
554
610
|
if (!this.consoleTabsRepository) return;
|
|
555
611
|
const targetTabName = this.resolveConsoleTargetTab(issue.status ?? '');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mock } from 'jest-mock-extended';
|
|
2
2
|
import { FieldOption, Project } from '../entities/Project';
|
|
3
3
|
import {
|
|
4
|
+
AGENT_FIELD_NAME,
|
|
4
5
|
DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
5
6
|
NEXT_ACTION_DATE_FIELD_NAME,
|
|
6
7
|
NEXT_ACTION_HOUR_FIELD_NAME,
|
|
@@ -27,6 +28,7 @@ describe('ProjectRequiredFieldCreateUseCase', () => {
|
|
|
27
28
|
remainingEstimationMinutes: null,
|
|
28
29
|
dependedIssueUrlSeparatedByComma: null,
|
|
29
30
|
completionDate50PercentConfidence: null,
|
|
31
|
+
agent: null,
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const buildProjectWithStory = (stories: FieldOption[]): Project => ({
|
|
@@ -50,13 +52,18 @@ describe('ProjectRequiredFieldCreateUseCase', () => {
|
|
|
50
52
|
mock<
|
|
51
53
|
Pick<
|
|
52
54
|
ProjectRepository,
|
|
53
|
-
|
|
55
|
+
| 'getByUrl'
|
|
56
|
+
| 'listFieldNames'
|
|
57
|
+
| 'createField'
|
|
58
|
+
| 'updateStoryList'
|
|
59
|
+
| 'updateAgentList'
|
|
54
60
|
>
|
|
55
61
|
>();
|
|
56
62
|
projectRepository.getByUrl.mockResolvedValue(project);
|
|
57
63
|
projectRepository.listFieldNames.mockResolvedValue(existingFieldNames);
|
|
58
64
|
projectRepository.createField.mockResolvedValue(undefined);
|
|
59
65
|
projectRepository.updateStoryList.mockResolvedValue([]);
|
|
66
|
+
projectRepository.updateAgentList.mockResolvedValue([]);
|
|
60
67
|
return {
|
|
61
68
|
projectRepository,
|
|
62
69
|
useCase: new ProjectRequiredFieldCreateUseCase(projectRepository),
|
|
@@ -592,4 +599,82 @@ describe('ProjectRequiredFieldCreateUseCase', () => {
|
|
|
592
599
|
);
|
|
593
600
|
});
|
|
594
601
|
});
|
|
602
|
+
|
|
603
|
+
describe('reconcileAgentOptions', () => {
|
|
604
|
+
const buildProjectWithAgent = (
|
|
605
|
+
existingOptions: { id: string; name: string }[],
|
|
606
|
+
): Project => ({
|
|
607
|
+
...projectWithoutStory,
|
|
608
|
+
agent: {
|
|
609
|
+
name: 'Agent',
|
|
610
|
+
fieldId: 'agent-field-id',
|
|
611
|
+
options: existingOptions.map((o) => ({
|
|
612
|
+
...o,
|
|
613
|
+
color: 'GRAY' as const,
|
|
614
|
+
description: '',
|
|
615
|
+
})),
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('adds missing agent names as GRAY options and calls updateAgentList', async () => {
|
|
620
|
+
const project = buildProjectWithAgent([{ id: 'opt-impl', name: 'impl' }]);
|
|
621
|
+
const { projectRepository, useCase } = createUseCase([], project);
|
|
622
|
+
|
|
623
|
+
await useCase.reconcileAgentOptions(project, ['impl', 'chore']);
|
|
624
|
+
|
|
625
|
+
expect(projectRepository.updateAgentList).toHaveBeenCalledWith(
|
|
626
|
+
project,
|
|
627
|
+
expect.arrayContaining([
|
|
628
|
+
expect.objectContaining({ id: 'opt-impl', name: 'impl' }),
|
|
629
|
+
expect.objectContaining({ id: null, name: 'chore', color: 'GRAY' }),
|
|
630
|
+
]),
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('does not call updateAgentList when all agents already exist', async () => {
|
|
635
|
+
const project = buildProjectWithAgent([
|
|
636
|
+
{ id: 'opt-impl', name: 'impl' },
|
|
637
|
+
{ id: 'opt-chore', name: 'chore' },
|
|
638
|
+
]);
|
|
639
|
+
const { projectRepository, useCase } = createUseCase([], project);
|
|
640
|
+
|
|
641
|
+
await useCase.reconcileAgentOptions(project, ['impl', 'chore']);
|
|
642
|
+
|
|
643
|
+
expect(projectRepository.updateAgentList).not.toHaveBeenCalled();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('does not call updateAgentList when agentNames is null', async () => {
|
|
647
|
+
const project = buildProjectWithAgent([]);
|
|
648
|
+
const { projectRepository, useCase } = createUseCase([], project);
|
|
649
|
+
|
|
650
|
+
await useCase.reconcileAgentOptions(project, null);
|
|
651
|
+
|
|
652
|
+
expect(projectRepository.updateAgentList).not.toHaveBeenCalled();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('calls createField with AGENT_FIELD_NAME and initial agents when project has no agent field', async () => {
|
|
656
|
+
const { projectRepository, useCase } = createUseCase(
|
|
657
|
+
[],
|
|
658
|
+
projectWithoutStory,
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
await useCase.reconcileAgentOptions(projectWithoutStory, [
|
|
662
|
+
'impl',
|
|
663
|
+
'chore',
|
|
664
|
+
]);
|
|
665
|
+
|
|
666
|
+
expect(projectRepository.updateAgentList).not.toHaveBeenCalled();
|
|
667
|
+
expect(projectRepository.createField).toHaveBeenCalledWith(
|
|
668
|
+
projectWithoutStory,
|
|
669
|
+
{
|
|
670
|
+
name: AGENT_FIELD_NAME,
|
|
671
|
+
dataType: 'SINGLE_SELECT',
|
|
672
|
+
options: [
|
|
673
|
+
{ name: 'impl', color: 'GRAY', description: '' },
|
|
674
|
+
{ name: 'chore', color: 'GRAY', description: '' },
|
|
675
|
+
],
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
595
680
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { FieldOption, Project } from '../entities/Project';
|
|
2
2
|
import { normalizeProjectFieldName } from '../entities/ProjectFieldName';
|
|
3
3
|
import {
|
|
4
|
+
AGENT_FIELD_NAME,
|
|
4
5
|
REQUIRED_PROJECT_FIELDS,
|
|
5
6
|
STORY_FIELD_NAME,
|
|
6
7
|
} from '../entities/RequiredProjectField';
|
|
7
8
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
8
9
|
|
|
9
|
-
type
|
|
10
|
+
type OptionToSubmit = Omit<FieldOption, 'id'> & {
|
|
10
11
|
id: FieldOption['id'] | null;
|
|
11
12
|
};
|
|
12
13
|
|
|
@@ -14,14 +15,25 @@ export class ProjectRequiredFieldCreateUseCase {
|
|
|
14
15
|
constructor(
|
|
15
16
|
private readonly projectRepository: Pick<
|
|
16
17
|
ProjectRepository,
|
|
17
|
-
|
|
18
|
+
| 'getByUrl'
|
|
19
|
+
| 'listFieldNames'
|
|
20
|
+
| 'createField'
|
|
21
|
+
| 'updateStoryList'
|
|
22
|
+
| 'updateAgentList'
|
|
18
23
|
>,
|
|
19
24
|
) {}
|
|
20
25
|
|
|
21
|
-
run = async (params: {
|
|
26
|
+
run = async (params: {
|
|
27
|
+
projectUrl: string;
|
|
28
|
+
agents?: string[] | null;
|
|
29
|
+
}): Promise<void> => {
|
|
22
30
|
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
23
31
|
await this.createMissingFields(project);
|
|
24
|
-
await this.
|
|
32
|
+
const updatedProject = await this.projectRepository.getByUrl(
|
|
33
|
+
params.projectUrl,
|
|
34
|
+
);
|
|
35
|
+
await this.reconcileStoryOptions(updatedProject);
|
|
36
|
+
await this.reconcileAgentOptions(updatedProject, params.agents ?? null);
|
|
25
37
|
};
|
|
26
38
|
|
|
27
39
|
private createMissingFields = async (project: Project): Promise<void> => {
|
|
@@ -51,9 +63,9 @@ export class ProjectRequiredFieldCreateUseCase {
|
|
|
51
63
|
return;
|
|
52
64
|
}
|
|
53
65
|
const requiredOptions = storyFieldDefinition.options;
|
|
54
|
-
const mergedOptions:
|
|
55
|
-
|
|
56
|
-
);
|
|
66
|
+
const mergedOptions: OptionToSubmit[] = project.story.stories.map((o) => ({
|
|
67
|
+
...o,
|
|
68
|
+
}));
|
|
57
69
|
let addedCount = 0;
|
|
58
70
|
let previousRequiredIndex = -1;
|
|
59
71
|
for (const required of requiredOptions) {
|
|
@@ -75,6 +87,47 @@ export class ProjectRequiredFieldCreateUseCase {
|
|
|
75
87
|
await this.projectRepository.updateStoryList(project, mergedOptions);
|
|
76
88
|
};
|
|
77
89
|
|
|
90
|
+
reconcileAgentOptions = async (
|
|
91
|
+
project: Project,
|
|
92
|
+
agentNames: string[] | null,
|
|
93
|
+
): Promise<void> => {
|
|
94
|
+
if (!agentNames || agentNames.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!project.agent) {
|
|
98
|
+
await this.projectRepository.createField(project, {
|
|
99
|
+
name: AGENT_FIELD_NAME,
|
|
100
|
+
dataType: 'SINGLE_SELECT',
|
|
101
|
+
options: agentNames.map((name) => ({
|
|
102
|
+
name,
|
|
103
|
+
color: 'GRAY' as const,
|
|
104
|
+
description: '',
|
|
105
|
+
})),
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const existingOptions = project.agent.options;
|
|
110
|
+
const existingNames = new Set(
|
|
111
|
+
existingOptions.map((o) => normalizeProjectFieldName(o.name)),
|
|
112
|
+
);
|
|
113
|
+
const toAdd = agentNames.filter(
|
|
114
|
+
(name) => !existingNames.has(normalizeProjectFieldName(name)),
|
|
115
|
+
);
|
|
116
|
+
if (toAdd.length === 0) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const mergedOptions: OptionToSubmit[] = [
|
|
120
|
+
...existingOptions.map((o) => ({ ...o })),
|
|
121
|
+
...toAdd.map((name) => ({
|
|
122
|
+
id: null,
|
|
123
|
+
name,
|
|
124
|
+
color: 'GRAY' as const,
|
|
125
|
+
description: '',
|
|
126
|
+
})),
|
|
127
|
+
];
|
|
128
|
+
await this.projectRepository.updateAgentList(project, mergedOptions);
|
|
129
|
+
};
|
|
130
|
+
|
|
78
131
|
private optionNameSatisfies = (
|
|
79
132
|
currentName: string,
|
|
80
133
|
requiredName: string,
|
|
@@ -82,4 +135,6 @@ export class ProjectRequiredFieldCreateUseCase {
|
|
|
82
135
|
normalizeProjectFieldName(currentName).startsWith(
|
|
83
136
|
normalizeProjectFieldName(requiredName),
|
|
84
137
|
);
|
|
138
|
+
|
|
139
|
+
static readonly AGENT_FIELD_NAME = AGENT_FIELD_NAME;
|
|
85
140
|
}
|