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