github-issue-tower-defence-management 1.101.2 → 1.101.4

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -0
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  4. package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js +2 -1
  5. package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.js +2 -1
  7. package/bin/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.js.map +1 -1
  8. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +37 -5
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  10. package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js +7 -4
  11. package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js.map +1 -1
  12. package/bin/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.js +7 -4
  13. package/bin/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.js.map +1 -1
  14. package/package.json +1 -1
  15. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
  16. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +2 -0
  17. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts +3 -0
  18. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +4 -0
  19. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.ts +9 -2
  20. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +241 -0
  21. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +55 -7
  22. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +34 -0
  23. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.ts +12 -3
  24. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +66 -0
  25. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.ts +12 -4
  26. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  27. package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts +1 -0
  28. package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts.map +1 -1
  29. package/types/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.d.ts +1 -0
  30. package/types/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.d.ts.map +1 -1
  31. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  32. package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts +1 -0
  33. package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts.map +1 -1
  34. package/types/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.d.ts +1 -0
  35. package/types/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.d.ts.map +1 -1
@@ -1149,6 +1149,247 @@ describe('ApiV3CheerioRestIssueRepository', () => {
1149
1149
  });
1150
1150
  });
1151
1151
 
1152
+ describe('getOpenPullRequest CI state computation', () => {
1153
+ afterEach(() => {
1154
+ jest.restoreAllMocks();
1155
+ });
1156
+
1157
+ const buildGraphqlPrResponse = (
1158
+ checkRunNodes: Array<{
1159
+ __typename: 'CheckRun';
1160
+ databaseId: number;
1161
+ name: string;
1162
+ conclusion: string | null;
1163
+ }>,
1164
+ ) => ({
1165
+ data: {
1166
+ repository: {
1167
+ pullRequest: {
1168
+ url: 'https://github.com/HiromiShikata/test-repository/pull/31',
1169
+ state: 'OPEN',
1170
+ isDraft: false,
1171
+ headRefName: 'dependabot/npm_and_yarn/some-package-2.0.0',
1172
+ baseRefName: 'main',
1173
+ mergeable: 'MERGEABLE',
1174
+ baseRepository: {
1175
+ branchProtectionRules: { nodes: [] },
1176
+ defaultBranchRef: { name: 'main' },
1177
+ rulesets: { nodes: [] },
1178
+ },
1179
+ commits: {
1180
+ nodes: [
1181
+ {
1182
+ commit: {
1183
+ statusCheckRollup: {
1184
+ contexts: {
1185
+ nodes: checkRunNodes,
1186
+ },
1187
+ },
1188
+ },
1189
+ },
1190
+ ],
1191
+ },
1192
+ reviewThreads: { nodes: [] },
1193
+ },
1194
+ },
1195
+ },
1196
+ });
1197
+
1198
+ it('returns isCiStateSuccess true when latest CheckRun per name is SUCCESS even though an older run has FAILURE', async () => {
1199
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1200
+ new Response(
1201
+ JSON.stringify(
1202
+ buildGraphqlPrResponse([
1203
+ {
1204
+ __typename: 'CheckRun',
1205
+ databaseId: 100,
1206
+ name: 'check_pull_requests_to_link_issues',
1207
+ conclusion: 'FAILURE',
1208
+ },
1209
+ {
1210
+ __typename: 'CheckRun',
1211
+ databaseId: 200,
1212
+ name: 'check_pull_requests_to_link_issues',
1213
+ conclusion: 'SUCCESS',
1214
+ },
1215
+ ]),
1216
+ ),
1217
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1218
+ ),
1219
+ );
1220
+
1221
+ const { repository } = createApiV3CheerioRestIssueRepository();
1222
+ const result = await repository.getOpenPullRequest(
1223
+ 'https://github.com/HiromiShikata/test-repository/pull/31',
1224
+ );
1225
+
1226
+ expect(result).not.toBeNull();
1227
+ expect(result?.isCiStateSuccess).toBe(true);
1228
+ expect(result?.isPassedAllCiJob).toBe(true);
1229
+ });
1230
+
1231
+ it('returns isCiStateSuccess false when latest CheckRun per name has FAILURE conclusion', async () => {
1232
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1233
+ new Response(
1234
+ JSON.stringify(
1235
+ buildGraphqlPrResponse([
1236
+ {
1237
+ __typename: 'CheckRun',
1238
+ databaseId: 100,
1239
+ name: 'ci',
1240
+ conclusion: 'SUCCESS',
1241
+ },
1242
+ {
1243
+ __typename: 'CheckRun',
1244
+ databaseId: 200,
1245
+ name: 'ci',
1246
+ conclusion: 'FAILURE',
1247
+ },
1248
+ ]),
1249
+ ),
1250
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1251
+ ),
1252
+ );
1253
+
1254
+ const { repository } = createApiV3CheerioRestIssueRepository();
1255
+ const result = await repository.getOpenPullRequest(
1256
+ 'https://github.com/HiromiShikata/test-repository/pull/31',
1257
+ );
1258
+
1259
+ expect(result).not.toBeNull();
1260
+ expect(result?.isCiStateSuccess).toBe(false);
1261
+ expect(result?.isPassedAllCiJob).toBe(false);
1262
+ });
1263
+
1264
+ it('returns isCiStateSuccess false when latest CheckRun per name has null conclusion (still running)', async () => {
1265
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1266
+ new Response(
1267
+ JSON.stringify(
1268
+ buildGraphqlPrResponse([
1269
+ {
1270
+ __typename: 'CheckRun',
1271
+ databaseId: 100,
1272
+ name: 'ci',
1273
+ conclusion: 'FAILURE',
1274
+ },
1275
+ {
1276
+ __typename: 'CheckRun',
1277
+ databaseId: 200,
1278
+ name: 'ci',
1279
+ conclusion: null,
1280
+ },
1281
+ ]),
1282
+ ),
1283
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1284
+ ),
1285
+ );
1286
+
1287
+ const { repository } = createApiV3CheerioRestIssueRepository();
1288
+ const result = await repository.getOpenPullRequest(
1289
+ 'https://github.com/HiromiShikata/test-repository/pull/31',
1290
+ );
1291
+
1292
+ expect(result).not.toBeNull();
1293
+ expect(result?.isCiStateSuccess).toBe(false);
1294
+ expect(result?.isPassedAllCiJob).toBe(false);
1295
+ });
1296
+
1297
+ it('returns isCiStateSuccess false when a StatusContext has FAILURE state', async () => {
1298
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1299
+ new Response(
1300
+ JSON.stringify({
1301
+ data: {
1302
+ repository: {
1303
+ pullRequest: {
1304
+ url: 'https://github.com/HiromiShikata/test-repository/pull/31',
1305
+ state: 'OPEN',
1306
+ isDraft: false,
1307
+ headRefName: 'feature-branch',
1308
+ baseRefName: 'main',
1309
+ mergeable: 'MERGEABLE',
1310
+ baseRepository: {
1311
+ branchProtectionRules: { nodes: [] },
1312
+ defaultBranchRef: { name: 'main' },
1313
+ rulesets: { nodes: [] },
1314
+ },
1315
+ commits: {
1316
+ nodes: [
1317
+ {
1318
+ commit: {
1319
+ statusCheckRollup: {
1320
+ contexts: {
1321
+ nodes: [
1322
+ {
1323
+ __typename: 'StatusContext',
1324
+ context: 'external-ci',
1325
+ state: 'FAILURE',
1326
+ },
1327
+ ],
1328
+ },
1329
+ },
1330
+ },
1331
+ },
1332
+ ],
1333
+ },
1334
+ reviewThreads: { nodes: [] },
1335
+ },
1336
+ },
1337
+ },
1338
+ }),
1339
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1340
+ ),
1341
+ );
1342
+
1343
+ const { repository } = createApiV3CheerioRestIssueRepository();
1344
+ const result = await repository.getOpenPullRequest(
1345
+ 'https://github.com/HiromiShikata/test-repository/pull/31',
1346
+ );
1347
+
1348
+ expect(result).not.toBeNull();
1349
+ expect(result?.isCiStateSuccess).toBe(false);
1350
+ });
1351
+
1352
+ it('returns isCiStateSuccess false when statusCheckRollup is null', async () => {
1353
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
1354
+ new Response(
1355
+ JSON.stringify({
1356
+ data: {
1357
+ repository: {
1358
+ pullRequest: {
1359
+ url: 'https://github.com/HiromiShikata/test-repository/pull/31',
1360
+ state: 'OPEN',
1361
+ isDraft: false,
1362
+ headRefName: 'feature-branch',
1363
+ baseRefName: 'main',
1364
+ mergeable: 'MERGEABLE',
1365
+ baseRepository: {
1366
+ branchProtectionRules: { nodes: [] },
1367
+ defaultBranchRef: { name: 'main' },
1368
+ rulesets: { nodes: [] },
1369
+ },
1370
+ commits: {
1371
+ nodes: [{ commit: { statusCheckRollup: null } }],
1372
+ },
1373
+ reviewThreads: { nodes: [] },
1374
+ },
1375
+ },
1376
+ },
1377
+ }),
1378
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
1379
+ ),
1380
+ );
1381
+
1382
+ const { repository } = createApiV3CheerioRestIssueRepository();
1383
+ const result = await repository.getOpenPullRequest(
1384
+ 'https://github.com/HiromiShikata/test-repository/pull/31',
1385
+ );
1386
+
1387
+ expect(result).not.toBeNull();
1388
+ expect(result?.isCiStateSuccess).toBe(false);
1389
+ expect(result?.isPassedAllCiJob).toBe(false);
1390
+ });
1391
+ });
1392
+
1152
1393
  const createApiV3CheerioRestIssueRepository = () => {
1153
1394
  const apiV3IssueRepository = mock<ApiV3IssueRepository>();
1154
1395
  const restIssueRepository = mock<RestIssueRepository>();
@@ -74,13 +74,13 @@ type TimelineItem = {
74
74
  nodes: Array<{
75
75
  commit: {
76
76
  statusCheckRollup?: {
77
- state: string;
78
77
  contexts?: {
79
78
  nodes: Array<
80
79
  | {
81
80
  __typename: 'CheckRun';
82
81
  name: string;
83
82
  conclusion: string | null;
83
+ databaseId: number;
84
84
  }
85
85
  | {
86
86
  __typename: 'StatusContext';
@@ -163,13 +163,13 @@ type PrStatusComputationData = {
163
163
  nodes: Array<{
164
164
  commit: {
165
165
  statusCheckRollup?: {
166
- state: string;
167
166
  contexts?: {
168
167
  nodes: Array<
169
168
  | {
170
169
  __typename: 'CheckRun';
171
170
  name: string;
172
171
  conclusion: string | null;
172
+ databaseId: number;
173
173
  }
174
174
  | {
175
175
  __typename: 'StatusContext';
@@ -835,8 +835,8 @@ export class ApiV3CheerioRestIssueRepository
835
835
  ): RelatedPullRequest => {
836
836
  const isConflicted = data.mergeable === 'CONFLICTING';
837
837
  const lastCommit = data.commits?.nodes[0]?.commit;
838
- const ciState = lastCommit?.statusCheckRollup?.state;
839
- const contexts = lastCommit?.statusCheckRollup?.contexts?.nodes || [];
838
+ const statusCheckRollup = lastCommit?.statusCheckRollup;
839
+ const contexts = statusCheckRollup?.contexts?.nodes || [];
840
840
 
841
841
  const branchProtectionRules =
842
842
  data.baseRepository?.branchProtectionRules?.nodes || [];
@@ -911,7 +911,55 @@ export class ApiV3CheerioRestIssueRepository
911
911
  (name) => !seenContextNames.has(name),
912
912
  );
913
913
  const allRequiredChecksPassed = missingRequiredCheckNames.length === 0;
914
- const isCiStateSuccess = ciState === 'SUCCESS';
914
+
915
+ const latestCheckRunByName = new Map<
916
+ string,
917
+ { conclusion: string | null; databaseId: number }
918
+ >();
919
+ for (const ctx of contexts) {
920
+ if (ctx.__typename === 'CheckRun') {
921
+ const existing = latestCheckRunByName.get(ctx.name);
922
+ if (!existing || ctx.databaseId > existing.databaseId) {
923
+ latestCheckRunByName.set(ctx.name, {
924
+ conclusion: ctx.conclusion,
925
+ databaseId: ctx.databaseId,
926
+ });
927
+ }
928
+ }
929
+ }
930
+ const failureConclusions = new Set([
931
+ 'FAILURE',
932
+ 'CANCELLED',
933
+ 'TIMED_OUT',
934
+ 'ACTION_REQUIRED',
935
+ 'STARTUP_FAILURE',
936
+ 'STALE',
937
+ ]);
938
+ const isCiStateSuccess = (() => {
939
+ if (!statusCheckRollup) return false;
940
+ const latestRuns = [...latestCheckRunByName.values()];
941
+ const statusContexts = contexts.filter(
942
+ (
943
+ ctx,
944
+ ): ctx is {
945
+ __typename: 'StatusContext';
946
+ context: string;
947
+ state: string;
948
+ } => ctx.__typename === 'StatusContext',
949
+ );
950
+ const hasFailure =
951
+ latestRuns.some(
952
+ (r) => r.conclusion !== null && failureConclusions.has(r.conclusion),
953
+ ) ||
954
+ statusContexts.some(
955
+ (ctx) => ctx.state === 'FAILURE' || ctx.state === 'ERROR',
956
+ );
957
+ if (hasFailure) return false;
958
+ const hasPending =
959
+ latestRuns.some((r) => r.conclusion === null) ||
960
+ statusContexts.some((ctx) => ctx.state === 'PENDING');
961
+ return !hasPending;
962
+ })();
915
963
  const isPassedAllCiJob = isCiStateSuccess && allRequiredChecksPassed;
916
964
 
917
965
  const reviewThreads = data.reviewThreads?.nodes || [];
@@ -1006,11 +1054,11 @@ export class ApiV3CheerioRestIssueRepository
1006
1054
  nodes {
1007
1055
  commit {
1008
1056
  statusCheckRollup {
1009
- state
1010
1057
  contexts(first: 100) {
1011
1058
  nodes {
1012
1059
  __typename
1013
1060
  ... on CheckRun {
1061
+ databaseId
1014
1062
  name
1015
1063
  conclusion
1016
1064
  }
@@ -1198,11 +1246,11 @@ export class ApiV3CheerioRestIssueRepository
1198
1246
  nodes {
1199
1247
  commit {
1200
1248
  statusCheckRollup {
1201
- state
1202
1249
  contexts(first: 100) {
1203
1250
  nodes {
1204
1251
  __typename
1205
1252
  ... on CheckRun {
1253
+ databaseId
1206
1254
  name
1207
1255
  conclusion
1208
1256
  }
@@ -5,6 +5,7 @@ import { GenerateInTmuxByHumanDataUseCase } from './GenerateInTmuxByHumanDataUse
5
5
  const ASSIGNEE = 'owner-login';
6
6
  const CONSOLE_BASE_URL = 'https://console.example.test';
7
7
  const CONSOLE_TOKEN = 'test-token-value';
8
+ const NOW = new Date('2026-06-25T12:00:00.000Z');
8
9
 
9
10
  const storyOption = (
10
11
  id: string,
@@ -112,6 +113,7 @@ describe('GenerateInTmuxByHumanDataUseCase', () => {
112
113
  overrides.consoleToken === undefined
113
114
  ? CONSOLE_TOKEN
114
115
  : overrides.consoleToken,
116
+ now: NOW,
115
117
  });
116
118
 
117
119
  describe('item filter', () => {
@@ -139,6 +141,38 @@ describe('GenerateInTmuxByHumanDataUseCase', () => {
139
141
  const result = run([makeIssue({ assignees: ['other-person'] })]);
140
142
  expect(result.v1).toEqual([]);
141
143
  });
144
+
145
+ it('rejects issues whose next action date is in the future', () => {
146
+ const result = run([
147
+ makeIssue({
148
+ story: 'Story Alpha',
149
+ nextActionDate: new Date(NOW.getTime() + 24 * 60 * 60 * 1000),
150
+ }),
151
+ ]);
152
+ expect(result.v1).toEqual([]);
153
+ });
154
+
155
+ it('rejects issues whose next action hour is set', () => {
156
+ const result = run([
157
+ makeIssue({ story: 'Story Alpha', nextActionHour: 9 }),
158
+ ]);
159
+ expect(result.v1).toEqual([]);
160
+ });
161
+
162
+ it('keeps issues whose next action date is in the past and is now due', () => {
163
+ const result = run([
164
+ makeIssue({
165
+ story: 'Story Alpha',
166
+ nextActionDate: new Date(NOW.getTime() - 24 * 60 * 60 * 1000),
167
+ }),
168
+ ]);
169
+ expect(result.v1).toEqual([
170
+ {
171
+ story: 'Story Alpha',
172
+ urls: ['https://github.com/demo/repo/issues/1'],
173
+ },
174
+ ]);
175
+ });
142
176
  });
143
177
 
144
178
  describe('story grouping and ordering', () => {
@@ -57,6 +57,7 @@ export type GenerateInTmuxByHumanDataInput = {
57
57
  repo: string;
58
58
  consoleBaseUrl: string | null;
59
59
  consoleToken: string | null;
60
+ now: Date;
60
61
  };
61
62
 
62
63
  type InTmuxByHumanStoryGroup = {
@@ -78,6 +79,7 @@ export class GenerateInTmuxByHumanDataUseCase {
78
79
  repo,
79
80
  consoleBaseUrl,
80
81
  consoleToken,
82
+ now,
81
83
  } = input;
82
84
 
83
85
  const storyOrder = project.story
@@ -85,7 +87,7 @@ export class GenerateInTmuxByHumanDataUseCase {
85
87
  : [];
86
88
 
87
89
  const selectedIssues = issues.filter((issue) =>
88
- this.isInTmuxByHuman(issue, assigneeLogin),
90
+ this.isInTmuxByHuman(issue, assigneeLogin, now),
89
91
  );
90
92
 
91
93
  const groups = this.groupByStoryOrder(selectedIssues, storyOrder);
@@ -139,10 +141,17 @@ export class GenerateInTmuxByHumanDataUseCase {
139
141
  return { v1, v2, v3, v4 };
140
142
  };
141
143
 
142
- private isInTmuxByHuman = (issue: Issue, assigneeLogin: string): boolean =>
144
+ private isInTmuxByHuman = (
145
+ issue: Issue,
146
+ assigneeLogin: string,
147
+ now: Date,
148
+ ): boolean =>
143
149
  issue.status === IN_TMUX_BY_HUMAN_STATUS_NAME &&
144
150
  issue.isClosed === false &&
145
- issue.assignees.includes(assigneeLogin);
151
+ issue.assignees.includes(assigneeLogin) &&
152
+ (issue.nextActionDate === null ||
153
+ issue.nextActionDate.getTime() <= now.getTime()) &&
154
+ issue.nextActionHour === null;
146
155
 
147
156
  private groupByStoryOrder = (
148
157
  issues: Issue[],
@@ -8,6 +8,7 @@ import {
8
8
 
9
9
  const ASSIGNEE = 'owner-login';
10
10
  const LAUNCHER = 'cl';
11
+ const NOW = new Date('2026-06-25T12:00:00.000Z');
11
12
 
12
13
  let issueCounter = 0;
13
14
  const makeIssue = (overrides: Partial<Issue> = {}): Issue => {
@@ -82,6 +83,7 @@ describe('InTmuxByHumanSessionReconcileUseCase', () => {
82
83
  issues: [issue],
83
84
  assigneeLogin: ASSIGNEE,
84
85
  launcherCommand: LAUNCHER,
86
+ now: NOW,
85
87
  });
86
88
 
87
89
  expect(repository.launches).toEqual([
@@ -108,6 +110,7 @@ describe('InTmuxByHumanSessionReconcileUseCase', () => {
108
110
  issues: [issue],
109
111
  assigneeLogin: ASSIGNEE,
110
112
  launcherCommand: LAUNCHER,
113
+ now: NOW,
111
114
  });
112
115
 
113
116
  expect(repository.launches).toEqual([]);
@@ -126,6 +129,7 @@ describe('InTmuxByHumanSessionReconcileUseCase', () => {
126
129
  issues: [issue],
127
130
  assigneeLogin: ASSIGNEE,
128
131
  launcherCommand: LAUNCHER,
132
+ now: NOW,
129
133
  });
130
134
 
131
135
  expect(repository.launches).toHaveLength(1);
@@ -146,6 +150,7 @@ describe('InTmuxByHumanSessionReconcileUseCase', () => {
146
150
  issues: [liveIssue, missingIssueOne, missingIssueTwo],
147
151
  assigneeLogin: ASSIGNEE,
148
152
  launcherCommand: LAUNCHER,
153
+ now: NOW,
149
154
  });
150
155
 
151
156
  expect(result.launchedIssueUrls).toEqual([
@@ -172,12 +177,73 @@ describe('InTmuxByHumanSessionReconcileUseCase', () => {
172
177
  issues: [otherStatusIssue, closedIssue, otherAssigneeIssue],
173
178
  assigneeLogin: ASSIGNEE,
174
179
  launcherCommand: LAUNCHER,
180
+ now: NOW,
175
181
  });
176
182
 
177
183
  expect(repository.launches).toEqual([]);
178
184
  expect(result.launchedIssueUrls).toEqual([]);
179
185
  });
180
186
 
187
+ it('excludes an issue whose next action date is in the future', async () => {
188
+ const futureIssue = makeIssue({
189
+ nextActionDate: new Date(NOW.getTime() + 24 * 60 * 60 * 1000),
190
+ });
191
+ const repository = createFakeTmuxSessionRepository({
192
+ liveSessionNames: [],
193
+ processCommandLines: [],
194
+ });
195
+ const useCase = new InTmuxByHumanSessionReconcileUseCase(repository);
196
+
197
+ const result = await useCase.run({
198
+ issues: [futureIssue],
199
+ assigneeLogin: ASSIGNEE,
200
+ launcherCommand: LAUNCHER,
201
+ now: NOW,
202
+ });
203
+
204
+ expect(repository.launches).toEqual([]);
205
+ expect(result.launchedIssueUrls).toEqual([]);
206
+ });
207
+
208
+ it('excludes an issue whose next action hour is set', async () => {
209
+ const hourIssue = makeIssue({ nextActionHour: 9 });
210
+ const repository = createFakeTmuxSessionRepository({
211
+ liveSessionNames: [],
212
+ processCommandLines: [],
213
+ });
214
+ const useCase = new InTmuxByHumanSessionReconcileUseCase(repository);
215
+
216
+ const result = await useCase.run({
217
+ issues: [hourIssue],
218
+ assigneeLogin: ASSIGNEE,
219
+ launcherCommand: LAUNCHER,
220
+ now: NOW,
221
+ });
222
+
223
+ expect(repository.launches).toEqual([]);
224
+ expect(result.launchedIssueUrls).toEqual([]);
225
+ });
226
+
227
+ it('includes an issue whose next action date is in the past and is now due', async () => {
228
+ const dueIssue = makeIssue({
229
+ nextActionDate: new Date(NOW.getTime() - 24 * 60 * 60 * 1000),
230
+ });
231
+ const repository = createFakeTmuxSessionRepository({
232
+ liveSessionNames: [],
233
+ processCommandLines: [],
234
+ });
235
+ const useCase = new InTmuxByHumanSessionReconcileUseCase(repository);
236
+
237
+ const result = await useCase.run({
238
+ issues: [dueIssue],
239
+ assigneeLogin: ASSIGNEE,
240
+ launcherCommand: LAUNCHER,
241
+ now: NOW,
242
+ });
243
+
244
+ expect(result.launchedIssueUrls).toEqual([dueIssue.url]);
245
+ });
246
+
181
247
  it('transforms an issue url into the same session name tmux derives from the raw url, replacing only "." and ":" and keeping "/"', () => {
182
248
  expect(toTmuxSessionName('https://github.com/demo/repo/issues/42')).toBe(
183
249
  'https_//github_com/demo/repo/issues/42',
@@ -6,6 +6,7 @@ export type InTmuxByHumanSessionReconcileInput = {
6
6
  issues: Issue[];
7
7
  assigneeLogin: string;
8
8
  launcherCommand: string;
9
+ now: Date;
9
10
  };
10
11
 
11
12
  export type InTmuxByHumanSessionReconcileResult = {
@@ -29,10 +30,10 @@ export class InTmuxByHumanSessionReconcileUseCase {
29
30
  run = async (
30
31
  input: InTmuxByHumanSessionReconcileInput,
31
32
  ): Promise<InTmuxByHumanSessionReconcileResult> => {
32
- const { issues, assigneeLogin, launcherCommand } = input;
33
+ const { issues, assigneeLogin, launcherCommand, now } = input;
33
34
 
34
35
  const targetIssues = issues.filter((issue) =>
35
- this.isInTmuxByHuman(issue, assigneeLogin),
36
+ this.isInTmuxByHuman(issue, assigneeLogin, now),
36
37
  );
37
38
  if (targetIssues.length === 0) {
38
39
  return { launchedIssueUrls: [] };
@@ -62,10 +63,17 @@ export class InTmuxByHumanSessionReconcileUseCase {
62
63
  return { launchedIssueUrls };
63
64
  };
64
65
 
65
- private isInTmuxByHuman = (issue: Issue, assigneeLogin: string): boolean =>
66
+ private isInTmuxByHuman = (
67
+ issue: Issue,
68
+ assigneeLogin: string,
69
+ now: Date,
70
+ ): boolean =>
66
71
  issue.status === IN_TMUX_STATUS_NAME &&
67
72
  issue.isClosed === false &&
68
- issue.assignees.includes(assigneeLogin);
73
+ issue.assignees.includes(assigneeLogin) &&
74
+ (issue.nextActionDate === null ||
75
+ issue.nextActionDate.getTime() <= now.getTime()) &&
76
+ issue.nextActionHour === null;
69
77
 
70
78
  private hasLiveSession = (
71
79
  issueUrl: string,
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AA8B3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CA+WP;CACH"}
1
+ {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AA8B3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAmXP;CACH"}
@@ -11,6 +11,7 @@ export type InTmuxByHumanDataWriterParams = {
11
11
  repo: string;
12
12
  project: Project;
13
13
  issues: Issue[];
14
+ now: Date;
14
15
  };
15
16
  export declare const writeInTmuxByHumanData: (params: InTmuxByHumanDataWriterParams) => void;
16
17
  //# sourceMappingURL=inTmuxByHumanDataWriter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inTmuxByHumanDataWriter.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAMhE,MAAM,MAAM,6BAA6B,GAAG;IAC1C,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,kBAAkB,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB,CAAC;AAUF,eAAO,MAAM,sBAAsB,GACjC,QAAQ,6BAA6B,KACpC,IAsEF,CAAC"}
1
+ {"version":3,"file":"inTmuxByHumanDataWriter.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAMhE,MAAM,MAAM,6BAA6B,GAAG;IAC1C,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,kBAAkB,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAUF,eAAO,MAAM,sBAAsB,GACjC,QAAQ,6BAA6B,KACpC,IAwEF,CAAC"}
@@ -5,6 +5,7 @@ export type ReconcileInTmuxByHumanSessionsParams = {
5
5
  assigneeLogin: string;
6
6
  issues: Issue[];
7
7
  localCommandRunner: LocalCommandRunner;
8
+ now: Date;
8
9
  };
9
10
  export declare const reconcileInTmuxByHumanSessions: (params: ReconcileInTmuxByHumanSessionsParams) => Promise<void>;
10
11
  //# sourceMappingURL=inTmuxByHumanSessionReconciler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inTmuxByHumanSessionReconciler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAIpG,MAAM,MAAM,oCAAoC,GAAG;IACjD,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,kBAAkB,EAAE,kBAAkB,CAAC;CACxC,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,QAAQ,oCAAoC,KAC3C,OAAO,CAAC,IAAI,CAcd,CAAC"}
1
+ {"version":3,"file":"inTmuxByHumanSessionReconciler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gEAAgE,CAAC;AAIpG,MAAM,MAAM,oCAAoC,GAAG;IACjD,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,GAAG,EAAE,IAAI,CAAC;CACX,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,QAAQ,oCAAoC,KAC3C,OAAO,CAAC,IAAI,CAoBd,CAAC"}