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.
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js +2 -1
- package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.js +2 -1
- package/bin/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +37 -5
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js +7 -4
- package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js.map +1 -1
- package/bin/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.js +7 -4
- package/bin/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +2 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +4 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.ts +9 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +241 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +55 -7
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +34 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.ts +12 -3
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +66 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.ts +12 -4
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.d.ts +1 -0
- package/types/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts +1 -0
- package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts.map +1 -1
- package/types/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.d.ts +1 -0
- 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
|
|
839
|
-
const contexts =
|
|
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
|
-
|
|
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 = (
|
|
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 = (
|
|
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,
|
|
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;
|
|
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;
|
|
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"}
|