github-issue-tower-defence-management 1.185.1 → 1.186.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 +17 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/index.js +2 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/console/webServer.js +3 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.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/issue/ApiV3CheerioRestIssueRepository.js +36 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/entities/Project.js +11 -1
- package/bin/domain/entities/Project.js.map +1 -1
- package/bin/domain/entities/RequiredProjectField.js +3 -2
- package/bin/domain/entities/RequiredProjectField.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +5 -2
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +14 -10
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
- package/src/adapter/entry-points/console/ui/src/index.css +35 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/console/webServer.ts +7 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -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 +1 -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 +1 -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/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/entities/Project.ts +11 -2
- package/src/domain/entities/RequiredProjectField.ts +3 -1
- package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +111 -1
- package/src/domain/usecases/StartPreparationUseCase.ts +6 -2
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.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 +2 -1
- 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 +6 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
|
@@ -858,6 +858,126 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
858
858
|
});
|
|
859
859
|
});
|
|
860
860
|
|
|
861
|
+
describe('detectConflictEvenIfEvaluationSkipped option', () => {
|
|
862
|
+
it('should call findRelatedOpenPRs and return PULL_REQUEST_CONFLICTED when relatedOpenPrUrls is null and PR is conflicted', async () => {
|
|
863
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
864
|
+
createReadyPr('https://github.com/user/repo/pull/1', {
|
|
865
|
+
isConflicted: true,
|
|
866
|
+
}),
|
|
867
|
+
]);
|
|
868
|
+
|
|
869
|
+
const result = await evaluator.evaluate(
|
|
870
|
+
{
|
|
871
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
872
|
+
labels: [],
|
|
873
|
+
isPr: false,
|
|
874
|
+
agent: 'chore',
|
|
875
|
+
},
|
|
876
|
+
[],
|
|
877
|
+
{
|
|
878
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
879
|
+
relatedOpenPrUrls: null,
|
|
880
|
+
},
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
|
|
884
|
+
'https://github.com/user/repo/issues/1',
|
|
885
|
+
);
|
|
886
|
+
expect(result.rejections).toHaveLength(1);
|
|
887
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
|
|
888
|
+
expect(result.approvedPrUrl).toBeNull();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should call findRelatedOpenPRs and return no rejections when relatedOpenPrUrls is null and no PR is conflicted', async () => {
|
|
892
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
893
|
+
createReadyPr('https://github.com/user/repo/pull/1'),
|
|
894
|
+
]);
|
|
895
|
+
|
|
896
|
+
const result = await evaluator.evaluate(
|
|
897
|
+
{
|
|
898
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
899
|
+
labels: [],
|
|
900
|
+
isPr: false,
|
|
901
|
+
agent: 'chore',
|
|
902
|
+
},
|
|
903
|
+
[],
|
|
904
|
+
{
|
|
905
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
906
|
+
relatedOpenPrUrls: null,
|
|
907
|
+
},
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
|
|
911
|
+
'https://github.com/user/repo/issues/1',
|
|
912
|
+
);
|
|
913
|
+
expect(result.rejections).toHaveLength(0);
|
|
914
|
+
expect(result.approvedPrUrl).toBeNull();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('should use resolveOpenPrsFromUrls path and not call findRelatedOpenPRs when relatedOpenPrUrls is provided', async () => {
|
|
918
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue(
|
|
919
|
+
createReadyPr('https://github.com/user/repo/pull/5', {
|
|
920
|
+
isConflicted: true,
|
|
921
|
+
}),
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
const result = await evaluator.evaluate(
|
|
925
|
+
{
|
|
926
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
927
|
+
labels: [],
|
|
928
|
+
isPr: false,
|
|
929
|
+
agent: 'chore',
|
|
930
|
+
},
|
|
931
|
+
[],
|
|
932
|
+
{
|
|
933
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
934
|
+
relatedOpenPrUrls: ['https://github.com/user/repo/pull/5'],
|
|
935
|
+
},
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
939
|
+
expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
|
|
940
|
+
'https://github.com/user/repo/pull/5',
|
|
941
|
+
);
|
|
942
|
+
expect(result.rejections).toHaveLength(1);
|
|
943
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should emit only PULL_REQUEST_CONFLICTED and not PULL_REQUEST_IS_DRAFT or other types when evaluation is skipped', async () => {
|
|
947
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
948
|
+
createReadyPr('https://github.com/user/repo/pull/1', {
|
|
949
|
+
isConflicted: true,
|
|
950
|
+
isDraft: true,
|
|
951
|
+
isPassedAllCiJob: false,
|
|
952
|
+
isCiStateSuccess: false,
|
|
953
|
+
}),
|
|
954
|
+
]);
|
|
955
|
+
|
|
956
|
+
const result = await evaluator.evaluate(
|
|
957
|
+
{
|
|
958
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
959
|
+
labels: [],
|
|
960
|
+
isPr: false,
|
|
961
|
+
agent: 'chore',
|
|
962
|
+
},
|
|
963
|
+
[],
|
|
964
|
+
{
|
|
965
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
966
|
+
relatedOpenPrUrls: null,
|
|
967
|
+
},
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
expect(result.rejections).toHaveLength(1);
|
|
971
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
|
|
972
|
+
expect(result.rejections.map((r) => r.type)).not.toContain(
|
|
973
|
+
'PULL_REQUEST_IS_DRAFT',
|
|
974
|
+
);
|
|
975
|
+
expect(result.rejections.map((r) => r.type)).not.toContain(
|
|
976
|
+
'ANY_CI_JOB_FAILED_OR_IN_PROGRESS',
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
861
981
|
describe('change-target-must: label behavior', () => {
|
|
862
982
|
const prUrl = 'https://github.com/user/repo/pull/1';
|
|
863
983
|
|
|
@@ -37,6 +37,7 @@ export type EvaluateOptions = {
|
|
|
37
37
|
// The agent name treated as the developer agent for PR/CI/conflict checks.
|
|
38
38
|
// When null or undefined, defaults to 'developer'.
|
|
39
39
|
developerAgentName?: string | null;
|
|
40
|
+
detectConflictEvenIfEvaluationSkipped?: boolean;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export class IssueRejectionEvaluator {
|
|
@@ -186,6 +187,26 @@ export class IssueRejectionEvaluator {
|
|
|
186
187
|
approvedPrUrl = pr.url;
|
|
187
188
|
}
|
|
188
189
|
}
|
|
190
|
+
} else if (options.detectConflictEvenIfEvaluationSkipped) {
|
|
191
|
+
let prsToCheck: RelatedPullRequest[];
|
|
192
|
+
if (options.relatedOpenPrUrls != null) {
|
|
193
|
+
const resolved = await this.resolveOpenPrsFromUrls(
|
|
194
|
+
options.relatedOpenPrUrls,
|
|
195
|
+
options.resolvedOpenPrByUrl ?? null,
|
|
196
|
+
);
|
|
197
|
+
prsToCheck = resolved.prs;
|
|
198
|
+
} else {
|
|
199
|
+
prsToCheck = await this.issueRepository.findRelatedOpenPRs(issue.url);
|
|
200
|
+
}
|
|
201
|
+
for (const pr of prsToCheck) {
|
|
202
|
+
if (pr.isConflicted) {
|
|
203
|
+
rejections.push({
|
|
204
|
+
type: 'PULL_REQUEST_CONFLICTED',
|
|
205
|
+
detail: `PULL_REQUEST_CONFLICTED: ${pr.url}`,
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
return { rejections, approvedPrUrl };
|
|
@@ -35,6 +35,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
|
35
35
|
author: '',
|
|
36
36
|
closingIssueReferenceUrls: [],
|
|
37
37
|
agent: null,
|
|
38
|
+
stateReason: null,
|
|
38
39
|
...overrides,
|
|
39
40
|
});
|
|
40
41
|
|
|
@@ -67,6 +68,7 @@ const createMergedPr = (
|
|
|
67
68
|
author: '',
|
|
68
69
|
closingIssueReferenceUrls: [closingIssueUrl],
|
|
69
70
|
agent: null,
|
|
71
|
+
stateReason: null,
|
|
70
72
|
...overrides,
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -384,4 +386,36 @@ describe('QualityCheckAdvanceUseCase', () => {
|
|
|
384
386
|
'done-id',
|
|
385
387
|
);
|
|
386
388
|
});
|
|
389
|
+
|
|
390
|
+
it('does not advance an issue with stateReason REOPENED even when it has a merged PR', async () => {
|
|
391
|
+
const issue = createMockIssue({ stateReason: 'REOPENED' });
|
|
392
|
+
const mergedPr = createMergedPr(issue.url);
|
|
393
|
+
const project = createMockProject();
|
|
394
|
+
|
|
395
|
+
await useCase.run({
|
|
396
|
+
project,
|
|
397
|
+
issues: [issue, mergedPr],
|
|
398
|
+
evaluatedAt: FIXED_NOW,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('advances an issue whose stateReason is COMPLETED even though it was previously REOPENED', async () => {
|
|
405
|
+
const issue = createMockIssue({ stateReason: 'COMPLETED' });
|
|
406
|
+
const mergedPr = createMergedPr(issue.url);
|
|
407
|
+
const project = createMockProject();
|
|
408
|
+
|
|
409
|
+
await useCase.run({
|
|
410
|
+
project,
|
|
411
|
+
issues: [issue, mergedPr],
|
|
412
|
+
evaluatedAt: FIXED_NOW,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
416
|
+
project,
|
|
417
|
+
issue,
|
|
418
|
+
'done-id',
|
|
419
|
+
);
|
|
420
|
+
});
|
|
387
421
|
});
|
|
@@ -45,6 +45,7 @@ export class QualityCheckAdvanceUseCase {
|
|
|
45
45
|
(issue) =>
|
|
46
46
|
issue.status === qualityCheckStatusName &&
|
|
47
47
|
!issue.isClosed &&
|
|
48
|
+
issue.stateReason !== 'REOPENED' &&
|
|
48
49
|
issue.dependedIssueUrls.length === 0 &&
|
|
49
50
|
!issueReactivationTriggerIsPending(issue, evaluatedAt) &&
|
|
50
51
|
issueUrlsWithMergedPr.has(issue.url),
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
|
|
2
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
|
+
import { Issue } from '../entities/Issue';
|
|
4
|
+
import { Project } from '../entities/Project';
|
|
5
|
+
import {
|
|
6
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
7
|
+
DONE_STATUS_NAME,
|
|
8
|
+
} from '../entities/WorkflowStatus';
|
|
9
|
+
|
|
10
|
+
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
11
|
+
|
|
12
|
+
const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
13
|
+
nameWithOwner: 'user/repo',
|
|
14
|
+
number: 1,
|
|
15
|
+
title: 'Test Issue',
|
|
16
|
+
state: 'OPEN',
|
|
17
|
+
status: DONE_STATUS_NAME,
|
|
18
|
+
story: 'Default Story',
|
|
19
|
+
nextActionDate: null,
|
|
20
|
+
nextActionHour: null,
|
|
21
|
+
estimationMinutes: null,
|
|
22
|
+
dependedIssueUrls: [],
|
|
23
|
+
completionDate50PercentConfidence: null,
|
|
24
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
25
|
+
assignees: [],
|
|
26
|
+
labels: [],
|
|
27
|
+
org: 'user',
|
|
28
|
+
repo: 'repo',
|
|
29
|
+
body: '',
|
|
30
|
+
itemId: 'item-1',
|
|
31
|
+
isPr: false,
|
|
32
|
+
isInProgress: false,
|
|
33
|
+
isClosed: false,
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
author: '',
|
|
36
|
+
closingIssueReferenceUrls: [],
|
|
37
|
+
agent: null,
|
|
38
|
+
stateReason: 'REOPENED',
|
|
39
|
+
...overrides,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const createMockProject = (): Project => ({
|
|
43
|
+
id: 'project-1',
|
|
44
|
+
url: 'https://github.com/orgs/user/projects/1',
|
|
45
|
+
databaseId: 1,
|
|
46
|
+
name: 'Test Project',
|
|
47
|
+
status: {
|
|
48
|
+
name: 'Status',
|
|
49
|
+
fieldId: 'status-field-id',
|
|
50
|
+
statuses: [
|
|
51
|
+
{
|
|
52
|
+
id: 'awaiting-workspace-id',
|
|
53
|
+
name: AWAITING_WORKSPACE_STATUS_NAME,
|
|
54
|
+
color: 'BLUE',
|
|
55
|
+
description: '',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'preparation-id',
|
|
59
|
+
name: 'Preparation',
|
|
60
|
+
color: 'YELLOW',
|
|
61
|
+
description: '',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'done-id',
|
|
65
|
+
name: DONE_STATUS_NAME,
|
|
66
|
+
color: 'PURPLE',
|
|
67
|
+
description: '',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
nextActionDate: null,
|
|
72
|
+
nextActionHour: null,
|
|
73
|
+
story: null,
|
|
74
|
+
remainingEstimationMinutes: null,
|
|
75
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
76
|
+
completionDate50PercentConfidence: null,
|
|
77
|
+
agent: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('ReopenedDoneIssueRevertUseCase', () => {
|
|
81
|
+
let useCase: ReopenedDoneIssueRevertUseCase;
|
|
82
|
+
let mockIssueRepository: Mocked<Pick<IssueRepository, 'updateStatus'>>;
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
mockIssueRepository = {
|
|
86
|
+
updateStatus: jest.fn(),
|
|
87
|
+
};
|
|
88
|
+
useCase = new ReopenedDoneIssueRevertUseCase(mockIssueRepository);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('resets a Done issue with stateReason REOPENED to Awaiting Workspace', async () => {
|
|
92
|
+
const issue = createMockIssue();
|
|
93
|
+
const project = createMockProject();
|
|
94
|
+
|
|
95
|
+
await useCase.run({ project, issues: [issue] });
|
|
96
|
+
|
|
97
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
98
|
+
project,
|
|
99
|
+
issue,
|
|
100
|
+
'awaiting-workspace-id',
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not reset a Done issue with stateReason COMPLETED', async () => {
|
|
105
|
+
const issue = createMockIssue({ stateReason: 'COMPLETED' });
|
|
106
|
+
const project = createMockProject();
|
|
107
|
+
|
|
108
|
+
await useCase.run({ project, issues: [issue] });
|
|
109
|
+
|
|
110
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not reset a Done issue with stateReason null', async () => {
|
|
114
|
+
const issue = createMockIssue({ stateReason: null });
|
|
115
|
+
const project = createMockProject();
|
|
116
|
+
|
|
117
|
+
await useCase.run({ project, issues: [issue] });
|
|
118
|
+
|
|
119
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not reset a PR item even when Done and stateReason is REOPENED', async () => {
|
|
123
|
+
const prIssue = createMockIssue({
|
|
124
|
+
isPr: true,
|
|
125
|
+
url: 'https://github.com/user/repo/pull/10',
|
|
126
|
+
});
|
|
127
|
+
const project = createMockProject();
|
|
128
|
+
|
|
129
|
+
await useCase.run({ project, issues: [prIssue] });
|
|
130
|
+
|
|
131
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not reset an issue that is not in Done status', async () => {
|
|
135
|
+
const issue = createMockIssue({ status: 'Awaiting Workspace' });
|
|
136
|
+
const project = createMockProject();
|
|
137
|
+
|
|
138
|
+
await useCase.run({ project, issues: [issue] });
|
|
139
|
+
|
|
140
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('logs an error and returns 0 when Awaiting Workspace status option is missing', async () => {
|
|
144
|
+
const issue = createMockIssue();
|
|
145
|
+
const project = createMockProject();
|
|
146
|
+
project.status.statuses = project.status.statuses.filter(
|
|
147
|
+
(s) => s.name !== AWAITING_WORKSPACE_STATUS_NAME,
|
|
148
|
+
);
|
|
149
|
+
const consoleSpy = jest
|
|
150
|
+
.spyOn(console, 'error')
|
|
151
|
+
.mockImplementation(() => {});
|
|
152
|
+
|
|
153
|
+
const result = await useCase.run({ project, issues: [issue] });
|
|
154
|
+
|
|
155
|
+
expect(result).toBe(0);
|
|
156
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
157
|
+
consoleSpy.mockRestore();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('collects updateStatus failures and re-throws as AggregateError after processing all items', async () => {
|
|
161
|
+
const issue1 = createMockIssue({
|
|
162
|
+
number: 1,
|
|
163
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
164
|
+
itemId: 'item-1',
|
|
165
|
+
});
|
|
166
|
+
const issue2 = createMockIssue({
|
|
167
|
+
number: 2,
|
|
168
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
169
|
+
itemId: 'item-2',
|
|
170
|
+
});
|
|
171
|
+
const project = createMockProject();
|
|
172
|
+
const updateError = new Error('updateStatus failed for issue1');
|
|
173
|
+
mockIssueRepository.updateStatus
|
|
174
|
+
.mockRejectedValueOnce(updateError)
|
|
175
|
+
.mockResolvedValueOnce(undefined);
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
useCase.run({ project, issues: [issue1, issue2] }),
|
|
179
|
+
).rejects.toBeInstanceOf(AggregateError);
|
|
180
|
+
|
|
181
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(2);
|
|
182
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
183
|
+
project,
|
|
184
|
+
issue2,
|
|
185
|
+
'awaiting-workspace-id',
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Issue } from '../entities/Issue';
|
|
2
|
+
import { Project } from '../entities/Project';
|
|
3
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
|
+
import {
|
|
5
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
6
|
+
DONE_STATUS_NAME,
|
|
7
|
+
} from '../entities/WorkflowStatus';
|
|
8
|
+
|
|
9
|
+
export class ReopenedDoneIssueRevertUseCase {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly issueRepository: Pick<IssueRepository, 'updateStatus'>,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
run = async (params: {
|
|
15
|
+
project: Project;
|
|
16
|
+
issues: Issue[];
|
|
17
|
+
}): Promise<number> => {
|
|
18
|
+
const awaitingWorkspaceStatusOption = params.project.status.statuses.find(
|
|
19
|
+
(s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
|
|
20
|
+
);
|
|
21
|
+
if (!awaitingWorkspaceStatusOption) {
|
|
22
|
+
console.error(
|
|
23
|
+
`Awaiting Workspace status option '${AWAITING_WORKSPACE_STATUS_NAME}' not found in project.`,
|
|
24
|
+
);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const itemsToRevert = params.issues.filter(
|
|
29
|
+
(issue) =>
|
|
30
|
+
issue.status === DONE_STATUS_NAME &&
|
|
31
|
+
issue.stateReason === 'REOPENED' &&
|
|
32
|
+
!issue.isPr,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
let revertedCount = 0;
|
|
36
|
+
const errors: unknown[] = [];
|
|
37
|
+
for (const issue of itemsToRevert) {
|
|
38
|
+
try {
|
|
39
|
+
await this.issueRepository.updateStatus(
|
|
40
|
+
params.project,
|
|
41
|
+
issue,
|
|
42
|
+
awaitingWorkspaceStatusOption.id,
|
|
43
|
+
);
|
|
44
|
+
revertedCount++;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
errors.push(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (errors.length > 0) {
|
|
50
|
+
throw new AggregateError(
|
|
51
|
+
errors,
|
|
52
|
+
`Failed to revert ${errors.length} issue(s) from ${DONE_STATUS_NAME} to ${AWAITING_WORKSPACE_STATUS_NAME}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return revertedCount;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -78,6 +78,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
|
78
78
|
author: 'owner',
|
|
79
79
|
closingIssueReferenceUrls: [],
|
|
80
80
|
agent: null,
|
|
81
|
+
stateReason: null,
|
|
81
82
|
...overrides,
|
|
82
83
|
});
|
|
83
84
|
|
|
@@ -249,6 +250,49 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
|
|
|
249
250
|
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
250
251
|
});
|
|
251
252
|
|
|
253
|
+
it('should revert chore agent issue to Awaiting Workspace when its linked PR is conflicted', async () => {
|
|
254
|
+
const issue = createMockIssue({
|
|
255
|
+
status: 'Awaiting Quality Check',
|
|
256
|
+
agent: 'chore',
|
|
257
|
+
});
|
|
258
|
+
linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
|
|
259
|
+
{ ...createReadyPr(), isConflicted: true },
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
await useCase.run({
|
|
263
|
+
manager: 'manager-user',
|
|
264
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
265
|
+
allowedIssueAuthors: ['owner'],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
269
|
+
mockProject,
|
|
270
|
+
issue,
|
|
271
|
+
'awaiting-workspace-id',
|
|
272
|
+
);
|
|
273
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
274
|
+
issue,
|
|
275
|
+
expect.stringContaining('PULL_REQUEST_CONFLICTED'),
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should not revert chore agent issue when its linked PR is not conflicted', async () => {
|
|
280
|
+
const issue = createMockIssue({
|
|
281
|
+
status: 'Awaiting Quality Check',
|
|
282
|
+
agent: 'chore',
|
|
283
|
+
});
|
|
284
|
+
linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [createReadyPr()]);
|
|
285
|
+
|
|
286
|
+
await useCase.run({
|
|
287
|
+
manager: 'manager-user',
|
|
288
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
289
|
+
allowedIssueAuthors: ['owner'],
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
293
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
252
296
|
it('should revert Awaiting Quality Check issue with developer agent field and no linked PR', async () => {
|
|
253
297
|
const issue = createMockIssue({
|
|
254
298
|
status: 'Awaiting Quality Check',
|
|
@@ -98,22 +98,13 @@ export class RevertNotReadyReviewQueueIssueUseCase {
|
|
|
98
98
|
|
|
99
99
|
const labelsNotRequiringPullRequest =
|
|
100
100
|
resolveLabelsNotRequiringPullRequest(params);
|
|
101
|
-
const willBeEvaluated = (item: Issue): boolean =>
|
|
102
|
-
isAuthorAuthorizedForAutoStatusCheck(item.author, allowedIssueAuthors) &&
|
|
103
|
-
this.issueRejectionEvaluator.requiresPullRequestEvaluation(
|
|
104
|
-
item,
|
|
105
|
-
labelsNotRequiringPullRequest,
|
|
106
|
-
params.developerAgentName,
|
|
107
|
-
);
|
|
108
101
|
|
|
109
102
|
const resolvedOpenPrByUrl = await this.issueRepository.getOpenPullRequests(
|
|
110
103
|
Array.from(
|
|
111
104
|
new Set([
|
|
112
|
-
...awaitingQualityCheckIssues
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
(issue) => relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? [],
|
|
116
|
-
),
|
|
105
|
+
...awaitingQualityCheckIssues.flatMap(
|
|
106
|
+
(issue) => relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? [],
|
|
107
|
+
),
|
|
117
108
|
]),
|
|
118
109
|
),
|
|
119
110
|
);
|
|
@@ -135,6 +126,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
|
|
|
135
126
|
relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? null,
|
|
136
127
|
resolvedOpenPrByUrl,
|
|
137
128
|
developerAgentName: params.developerAgentName,
|
|
129
|
+
detectConflictEvenIfEvaluationSkipped: true,
|
|
138
130
|
},
|
|
139
131
|
);
|
|
140
132
|
if (
|