github-issue-tower-defence-management 1.163.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 +24 -0
- package/README.md +1 -1
- 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 +59 -5
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/RestIssueRepository.js +24 -0
- package/bin/adapter/repositories/issue/RestIssueRepository.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 +67 -0
- 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/lib/typescript-compat/bin/tsc +2 -0
- package/lib/typescript-compat/bin/tsserver +2 -0
- package/lib/typescript-compat/lib/typescript.js +1 -0
- package/lib/typescript-compat/package.json +13 -0
- package/package.json +4 -3
- 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 +4 -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 +99 -6
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/adapter/repositories/issue/RestIssueRepository.ts +30 -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 +381 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +117 -1
- 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 +10 -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 +5 -2
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/RestIssueRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/RestIssueRepository.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 -1
- 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 +2 -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,11 +89,14 @@ 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;
|
|
93
96
|
update: jest.Mock;
|
|
94
97
|
updateStatus: jest.Mock;
|
|
98
|
+
updateLabels: jest.Mock;
|
|
99
|
+
getOrCreateLabel: jest.Mock;
|
|
95
100
|
findRelatedOpenPRs: jest.Mock;
|
|
96
101
|
getStoryObjectMap: jest.Mock;
|
|
97
102
|
getOpenPullRequest: jest.Mock;
|
|
@@ -99,6 +104,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
99
104
|
approvePullRequest: jest.Mock;
|
|
100
105
|
requestChangesWithInlineComment: jest.Mock;
|
|
101
106
|
setDependedIssueUrl: jest.Mock;
|
|
107
|
+
setIssueAgentField: jest.Mock;
|
|
102
108
|
};
|
|
103
109
|
let mockIssueCommentRepository: {
|
|
104
110
|
getCommentsFromIssue: jest.Mock;
|
|
@@ -121,6 +127,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
121
127
|
|
|
122
128
|
mockProjectRepository = {
|
|
123
129
|
getByUrl: jest.fn(),
|
|
130
|
+
updateAgentList: jest.fn().mockResolvedValue([]),
|
|
124
131
|
};
|
|
125
132
|
|
|
126
133
|
mockIssueRepository = {
|
|
@@ -128,12 +135,15 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
128
135
|
get: jest.fn(),
|
|
129
136
|
update: jest.fn(),
|
|
130
137
|
updateStatus: jest.fn(),
|
|
138
|
+
updateLabels: jest.fn().mockResolvedValue(undefined),
|
|
139
|
+
getOrCreateLabel: jest.fn().mockResolvedValue(undefined),
|
|
131
140
|
findRelatedOpenPRs: jest.fn(),
|
|
132
141
|
getOpenPullRequest: jest.fn(),
|
|
133
142
|
getPullRequestChangedFilePaths: jest.fn().mockResolvedValue([]),
|
|
134
143
|
approvePullRequest: jest.fn().mockResolvedValue(undefined),
|
|
135
144
|
requestChangesWithInlineComment: jest.fn().mockResolvedValue(undefined),
|
|
136
145
|
setDependedIssueUrl: jest.fn(),
|
|
146
|
+
setIssueAgentField: jest.fn().mockResolvedValue(undefined),
|
|
137
147
|
};
|
|
138
148
|
|
|
139
149
|
mockIssueCommentRepository = {
|
|
@@ -799,6 +809,237 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
799
809
|
);
|
|
800
810
|
});
|
|
801
811
|
|
|
812
|
+
it('should add nextStepAgent label and return to Awaiting Workspace when report has nextStepAgent', async () => {
|
|
813
|
+
const issue = createMockIssue({
|
|
814
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
815
|
+
status: 'Preparation',
|
|
816
|
+
org: 'user',
|
|
817
|
+
repo: 'repo',
|
|
818
|
+
labels: [],
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
822
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
823
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
824
|
+
createMockComment({
|
|
825
|
+
content:
|
|
826
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:chore", "nextStep": null}\n```',
|
|
827
|
+
}),
|
|
828
|
+
]);
|
|
829
|
+
|
|
830
|
+
await useCase.run({
|
|
831
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
832
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
833
|
+
thresholdForAutoReject: 3,
|
|
834
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
835
|
+
allowedIssueAuthors: null,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
expect(mockIssueRepository.getOrCreateLabel).toHaveBeenCalledWith(
|
|
839
|
+
'user',
|
|
840
|
+
'repo',
|
|
841
|
+
'llm-agent:chore',
|
|
842
|
+
);
|
|
843
|
+
expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
|
|
844
|
+
expect.objectContaining({ labels: ['llm-agent:chore'] }),
|
|
845
|
+
['llm-agent:chore'],
|
|
846
|
+
);
|
|
847
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
848
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
849
|
+
mockProject,
|
|
850
|
+
);
|
|
851
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
852
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
853
|
+
expect.stringContaining('llm-agent:chore'),
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('should preserve existing labels when adding nextStepAgent label', async () => {
|
|
858
|
+
const issue = createMockIssue({
|
|
859
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
860
|
+
status: 'Preparation',
|
|
861
|
+
org: 'user',
|
|
862
|
+
repo: 'repo',
|
|
863
|
+
labels: ['existing-label'],
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
867
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
868
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
869
|
+
createMockComment({
|
|
870
|
+
content:
|
|
871
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:impl", "nextStep": null}\n```',
|
|
872
|
+
}),
|
|
873
|
+
]);
|
|
874
|
+
|
|
875
|
+
await useCase.run({
|
|
876
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
877
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
878
|
+
thresholdForAutoReject: 3,
|
|
879
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
880
|
+
allowedIssueAuthors: null,
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
|
|
884
|
+
expect.objectContaining({
|
|
885
|
+
labels: ['existing-label', 'llm-agent:impl'],
|
|
886
|
+
}),
|
|
887
|
+
['existing-label', 'llm-agent:impl'],
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should not add nextStepAgent label when nextStepAgent is null', async () => {
|
|
892
|
+
const issue = createMockIssue({
|
|
893
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
894
|
+
status: 'Preparation',
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
898
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
899
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
900
|
+
createMockComment({
|
|
901
|
+
content:
|
|
902
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": null, "nextStep": null}\n```',
|
|
903
|
+
}),
|
|
904
|
+
]);
|
|
905
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
906
|
+
{
|
|
907
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
908
|
+
isConflicted: false,
|
|
909
|
+
isPassedAllCiJob: true,
|
|
910
|
+
isCiStateSuccess: true,
|
|
911
|
+
isResolvedAllReviewComments: true,
|
|
912
|
+
isBranchOutOfDate: false,
|
|
913
|
+
missingRequiredCheckNames: [],
|
|
914
|
+
},
|
|
915
|
+
]);
|
|
916
|
+
|
|
917
|
+
await useCase.run({
|
|
918
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
919
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
920
|
+
thresholdForAutoReject: 3,
|
|
921
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
922
|
+
allowedIssueAuthors: null,
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
expect(mockIssueRepository.getOrCreateLabel).not.toHaveBeenCalled();
|
|
926
|
+
expect(mockIssueRepository.updateLabels).not.toHaveBeenCalled();
|
|
927
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
928
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
929
|
+
mockProject,
|
|
930
|
+
);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it('should create label when nextStepAgent label does not exist in repo', async () => {
|
|
934
|
+
const issue = createMockIssue({
|
|
935
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
936
|
+
status: 'Preparation',
|
|
937
|
+
org: 'user',
|
|
938
|
+
repo: 'repo',
|
|
939
|
+
labels: [],
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
943
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
944
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
945
|
+
createMockComment({
|
|
946
|
+
content:
|
|
947
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:new-agent", "nextStep": null}\n```',
|
|
948
|
+
}),
|
|
949
|
+
]);
|
|
950
|
+
|
|
951
|
+
await useCase.run({
|
|
952
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
953
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
954
|
+
thresholdForAutoReject: 3,
|
|
955
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
956
|
+
allowedIssueAuthors: null,
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
expect(mockIssueRepository.getOrCreateLabel).toHaveBeenCalledWith(
|
|
960
|
+
'user',
|
|
961
|
+
'repo',
|
|
962
|
+
'llm-agent:new-agent',
|
|
963
|
+
);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it('should remove existing llm-agent labels and labelsAsLlmAgentName labels when adding nextStepAgent label', async () => {
|
|
967
|
+
const issue = createMockIssue({
|
|
968
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
969
|
+
status: 'Preparation',
|
|
970
|
+
org: 'user',
|
|
971
|
+
repo: 'repo',
|
|
972
|
+
labels: ['llm-agent:chore', 'chore', 'feature-flag'],
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
976
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
977
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
978
|
+
createMockComment({
|
|
979
|
+
content:
|
|
980
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "llm-agent:impl", "nextStep": null}\n```',
|
|
981
|
+
}),
|
|
982
|
+
]);
|
|
983
|
+
|
|
984
|
+
await useCase.run({
|
|
985
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
986
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
987
|
+
thresholdForAutoReject: 3,
|
|
988
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
989
|
+
allowedIssueAuthors: null,
|
|
990
|
+
labelsAsLlmAgentName: ['chore', 'accounting'],
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
|
|
994
|
+
expect.objectContaining({
|
|
995
|
+
labels: ['feature-flag', 'llm-agent:impl'],
|
|
996
|
+
}),
|
|
997
|
+
['feature-flag', 'llm-agent:impl'],
|
|
998
|
+
);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('should not add nextStepAgent label when nextStepAgent is an empty string', async () => {
|
|
1002
|
+
const issue = createMockIssue({
|
|
1003
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1004
|
+
status: 'Preparation',
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1008
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1009
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1010
|
+
createMockComment({
|
|
1011
|
+
content:
|
|
1012
|
+
'From: :robot: Agent report\n```json\n{"nextStepAgent": "", "nextStep": null}\n```',
|
|
1013
|
+
}),
|
|
1014
|
+
]);
|
|
1015
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1016
|
+
{
|
|
1017
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1018
|
+
isConflicted: false,
|
|
1019
|
+
isPassedAllCiJob: true,
|
|
1020
|
+
isCiStateSuccess: true,
|
|
1021
|
+
isResolvedAllReviewComments: true,
|
|
1022
|
+
isBranchOutOfDate: false,
|
|
1023
|
+
missingRequiredCheckNames: [],
|
|
1024
|
+
},
|
|
1025
|
+
]);
|
|
1026
|
+
|
|
1027
|
+
await useCase.run({
|
|
1028
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1029
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1030
|
+
thresholdForAutoReject: 3,
|
|
1031
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1032
|
+
allowedIssueAuthors: null,
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
expect(mockIssueRepository.getOrCreateLabel).not.toHaveBeenCalled();
|
|
1036
|
+
expect(mockIssueRepository.updateLabels).not.toHaveBeenCalled();
|
|
1037
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1038
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
1039
|
+
mockProject,
|
|
1040
|
+
);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
802
1043
|
it('should auto-escalate to Failed Preparation after threshold rejections', async () => {
|
|
803
1044
|
const issue = createMockIssue({
|
|
804
1045
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -3364,6 +3605,146 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
3364
3605
|
});
|
|
3365
3606
|
});
|
|
3366
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
|
+
|
|
3367
3748
|
describe('consoleTabsRepository integration', () => {
|
|
3368
3749
|
let mockConsoleTabsRepository: {
|
|
3369
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,12 +54,17 @@ 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'
|
|
59
64
|
| 'update'
|
|
60
65
|
| 'updateStatus'
|
|
66
|
+
| 'updateLabels'
|
|
67
|
+
| 'getOrCreateLabel'
|
|
61
68
|
| 'findRelatedOpenPRs'
|
|
62
69
|
| 'getStoryObjectMap'
|
|
63
70
|
| 'getOpenPullRequest'
|
|
@@ -65,6 +72,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
65
72
|
| 'approvePullRequest'
|
|
66
73
|
| 'requestChangesWithInlineComment'
|
|
67
74
|
| 'setDependedIssueUrl'
|
|
75
|
+
| 'setIssueAgentField'
|
|
68
76
|
>,
|
|
69
77
|
private readonly issueCommentRepository: Pick<
|
|
70
78
|
IssueCommentRepository,
|
|
@@ -91,6 +99,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
91
99
|
labelsAsLlmAgentName?: string[] | null;
|
|
92
100
|
labelsNotRequiringPullRequest?: string[] | null;
|
|
93
101
|
changeTargetPathAliases?: Record<string, string> | null;
|
|
102
|
+
agents?: string[] | null;
|
|
94
103
|
}): Promise<void> => {
|
|
95
104
|
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
96
105
|
|
|
@@ -193,6 +202,55 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
193
202
|
const isTrustedAuthor = (author: string): boolean =>
|
|
194
203
|
this.isAuthorTrusted(author, params.allowedIssueAuthors ?? null);
|
|
195
204
|
|
|
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,
|
|
222
|
+
project,
|
|
223
|
+
agentOptionId,
|
|
224
|
+
);
|
|
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;
|
|
252
|
+
}
|
|
253
|
+
|
|
196
254
|
const { rejections, approvedPrUrl } = await this.collectRejections(
|
|
197
255
|
issue,
|
|
198
256
|
comments,
|
|
@@ -480,6 +538,64 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
480
538
|
return null;
|
|
481
539
|
};
|
|
482
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
|
+
|
|
483
599
|
private patchConsoleTab = async (issue: Issue): Promise<void> => {
|
|
484
600
|
if (!this.consoleTabsRepository) return;
|
|
485
601
|
const targetTabName = this.resolveConsoleTargetTab(issue.status ?? '');
|