github-issue-tower-defence-management 1.120.0 → 1.122.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 +14 -0
- package/README.md +8 -9
- package/bin/adapter/entry-points/cli/index.js +8 -14
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +0 -6
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +6 -12
- package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -10
- 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/machineStatusWriter.js +23 -19
- package/bin/adapter/entry-points/handlers/machineStatusWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/staleTmuxSessionCleaner.js +1 -2
- package/bin/adapter/entry-points/handlers/staleTmuxSessionCleaner.js.map +1 -1
- package/bin/adapter/repositories/LocalStorageCacheRepository.js +25 -0
- package/bin/adapter/repositories/LocalStorageCacheRepository.js.map +1 -1
- package/bin/adapter/repositories/ProcHostMetricsRepository.js +10 -5
- package/bin/adapter/repositories/ProcHostMetricsRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +119 -59
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -3
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/GetStoryObjectMapUseCase.js +1 -1
- package/bin/domain/usecases/GetStoryObjectMapUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -8
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +2 -2
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +1 -1
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
- package/bin/domain/usecases/StaleTmuxSessionKillUseCase.js +1 -1
- package/bin/domain/usecases/StaleTmuxSessionKillUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js +2 -2
- package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +0 -5
- package/src/adapter/entry-points/cli/index.ts +16 -17
- package/src/adapter/entry-points/cli/projectConfig.ts +0 -8
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +4 -25
- package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +4 -7
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +2 -25
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -4
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +11 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/machineStatusWriter.test.ts +29 -11
- package/src/adapter/entry-points/handlers/machineStatusWriter.ts +30 -19
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +0 -5
- package/src/adapter/entry-points/handlers/situationFileWriter.ts +0 -1
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +0 -5
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.ts +1 -9
- package/src/adapter/repositories/LocalStorageCacheRepository.test.ts +75 -0
- package/src/adapter/repositories/LocalStorageCacheRepository.ts +24 -0
- package/src/adapter/repositories/ProcHostMetricsRepository.test.ts +29 -8
- package/src/adapter/repositories/ProcHostMetricsRepository.ts +10 -6
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +198 -47
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +160 -81
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +23 -3
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +7 -12
- package/src/domain/usecases/GetStoryObjectMapUseCase.ts +1 -5
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +8 -24
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +6 -17
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +4 -8
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +35 -42
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +1 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +32 -32
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -5
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +17 -4
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +1 -4
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -7
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.ts +1 -5
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -94
- package/src/domain/usecases/StartPreparationUseCase.ts +2 -5
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +3 -10
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +11 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.ts +3 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +0 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/function/getStoryObjectMap.d.ts +1 -1
- package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts +1 -1
- package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.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/machineStatusWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/machineStatusWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +0 -1
- package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/staleTmuxSessionCleaner.d.ts +0 -1
- package/types/adapter/entry-points/handlers/staleTmuxSessionCleaner.d.ts.map +1 -1
- package/types/adapter/repositories/LocalStorageCacheRepository.d.ts +2 -0
- package/types/adapter/repositories/LocalStorageCacheRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProcHostMetricsRepository.d.ts +1 -1
- package/types/adapter/repositories/ProcHostMetricsRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +22 -7
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +2 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/usecases/GetStoryObjectMapUseCase.d.ts +0 -1
- package/types/domain/usecases/GetStoryObjectMapUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +0 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +0 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StaleTmuxSessionKillUseCase.d.ts +0 -1
- package/types/domain/usecases/StaleTmuxSessionKillUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +4 -3
- package/types/domain/usecases/adapter-interfaces/IssueRepository.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
|
@@ -104,6 +104,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
104
104
|
dashboardDataDir?: string;
|
|
105
105
|
workflowBlockerStoryName?: string;
|
|
106
106
|
inTmuxDataOutputDir?: string;
|
|
107
|
+
newIssueRepo?: string;
|
|
107
108
|
inTmuxConsoleBaseUrl?: string;
|
|
108
109
|
inTmuxConsoleToken?: string;
|
|
109
110
|
inTmuxProjectOrder?: string[];
|
|
@@ -182,8 +183,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
182
183
|
allowedIssueAuthors: normalizeAllowedIssueAuthors(
|
|
183
184
|
input.allowedIssueAuthors,
|
|
184
185
|
),
|
|
185
|
-
allowIssueCacheMinutes:
|
|
186
|
-
readmeConfig.allowIssueCacheMinutes ?? input.allowIssueCacheMinutes,
|
|
187
186
|
claudeCodeOauthTokenListJsonPath:
|
|
188
187
|
readmeConfig.claudeCodeOauthTokenListJsonPath ??
|
|
189
188
|
input.claudeCodeOauthTokenListJsonPath,
|
|
@@ -302,6 +301,8 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
302
301
|
restIssueRepository,
|
|
303
302
|
graphqlProjectItemRepository,
|
|
304
303
|
localStorageCacheRepository,
|
|
304
|
+
projectRepository,
|
|
305
|
+
systemDateRepository,
|
|
305
306
|
...githubRepositoryParams,
|
|
306
307
|
);
|
|
307
308
|
const setupTowerDefenceProjectUseCase = new SetupTowerDefenceProjectUseCase(
|
|
@@ -438,7 +439,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
438
439
|
mergedInput.startPreparation?.maximumPreparingIssuesCount ?? null,
|
|
439
440
|
utilizationPercentageThreshold:
|
|
440
441
|
mergedInput.startPreparation?.utilizationPercentageThreshold ?? 90,
|
|
441
|
-
allowIssueCacheMinutes: mergedInput.allowIssueCacheMinutes,
|
|
442
442
|
thresholdForAutoReject: 3,
|
|
443
443
|
},
|
|
444
444
|
preparationProcessCheckCommand:
|
|
@@ -523,6 +523,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
523
523
|
assigneeLogin: input.manager,
|
|
524
524
|
org: input.org,
|
|
525
525
|
repo: input.workingReport.repo,
|
|
526
|
+
newIssueRepo: mergedInput.newIssueRepo ?? undefined,
|
|
526
527
|
project: result.project,
|
|
527
528
|
issues: result.issues,
|
|
528
529
|
now: inTmuxNow,
|
|
@@ -554,7 +555,6 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
554
555
|
try {
|
|
555
556
|
await cleanStaleTmuxSessions({
|
|
556
557
|
project: result.project,
|
|
557
|
-
allowCacheMinutes: mergedInput.allowIssueCacheMinutes,
|
|
558
558
|
issueRepository,
|
|
559
559
|
localCommandRunner: nodeLocalCommandRunner,
|
|
560
560
|
now: inTmuxNow,
|
|
@@ -183,6 +183,17 @@ describe('writeInTmuxByHumanData', () => {
|
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
it('overrides the repo segment of the v4 new issue url when newIssueRepo is passed', () => {
|
|
187
|
+
writeInTmuxByHumanData({
|
|
188
|
+
...baseParams(outDir),
|
|
189
|
+
newIssueRepo: 'other-repo',
|
|
190
|
+
});
|
|
191
|
+
expect(readJson(file('demo.v4.json'))).toMatchObject({
|
|
192
|
+
newIssueUrl:
|
|
193
|
+
'https://github.com/demo-org/other-repo/issues/new?assignees=owner-login',
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
186
197
|
it('writes index files listing configured projects whose per-project file exists', () => {
|
|
187
198
|
fs.writeFileSync(file('xmile.json'), '[]\n');
|
|
188
199
|
writeInTmuxByHumanData({
|
|
@@ -16,6 +16,7 @@ export type InTmuxByHumanDataWriterParams = {
|
|
|
16
16
|
assigneeLogin: string | null | undefined;
|
|
17
17
|
org: string;
|
|
18
18
|
repo: string;
|
|
19
|
+
newIssueRepo?: string | null | undefined;
|
|
19
20
|
project: Project;
|
|
20
21
|
issues: Issue[];
|
|
21
22
|
now: Date;
|
|
@@ -41,6 +42,7 @@ export const writeInTmuxByHumanData = (
|
|
|
41
42
|
assigneeLogin,
|
|
42
43
|
org,
|
|
43
44
|
repo,
|
|
45
|
+
newIssueRepo,
|
|
44
46
|
project,
|
|
45
47
|
issues,
|
|
46
48
|
now,
|
|
@@ -56,6 +58,7 @@ export const writeInTmuxByHumanData = (
|
|
|
56
58
|
assigneeLogin,
|
|
57
59
|
org,
|
|
58
60
|
repo,
|
|
61
|
+
newIssueRepo: newIssueRepo ?? undefined,
|
|
59
62
|
consoleBaseUrl: inTmuxConsoleBaseUrl ?? null,
|
|
60
63
|
consoleToken: inTmuxConsoleToken ?? null,
|
|
61
64
|
now,
|
|
@@ -53,17 +53,23 @@ describe('writeMachineStatus', () => {
|
|
|
53
53
|
fs.rmSync(procDirectory, { recursive: true, force: true });
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it('writes machine-status.json with mem, cpu, disk, load and cycle minutes', async () => {
|
|
56
|
+
it('writes machine-status.json with mem, cpu, disk, load and cycle minutes derived from lastFetchedAt', async () => {
|
|
57
57
|
const { repository } = buildHostMetricsRepository(procDirectory);
|
|
58
58
|
const cacheDir = path.join(dir, 'allIssues-PVT');
|
|
59
59
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(cacheDir, 'latest.json'),
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
lastFetchedAt: '2026-06-26T12:00:00.000Z',
|
|
64
|
+
lastFullFetchAt: '2026-06-26T11:00:00.000Z',
|
|
65
|
+
project: {},
|
|
66
|
+
issues: [],
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
fs.writeFileSync(
|
|
70
|
+
path.join(dir, 'machine-status.json'),
|
|
71
|
+
JSON.stringify({ lastFetchedAt: '2026-06-26T11:45:00.000Z' }),
|
|
72
|
+
);
|
|
67
73
|
|
|
68
74
|
await writeMachineStatus({
|
|
69
75
|
dashboardDataDir: dir,
|
|
@@ -79,16 +85,25 @@ describe('writeMachineStatus', () => {
|
|
|
79
85
|
diskPct: 90,
|
|
80
86
|
load: [1.5, 0.8, 0.4],
|
|
81
87
|
cycleMinutes: 15,
|
|
88
|
+
lastFetchedAt: '2026-06-26T12:00:00.000Z',
|
|
82
89
|
capturedAt: '2026-06-26T12:00:00.000Z',
|
|
83
90
|
};
|
|
84
91
|
expect(written).toEqual(expected);
|
|
85
92
|
});
|
|
86
93
|
|
|
87
|
-
it('writes cycleMinutes null when
|
|
94
|
+
it('writes cycleMinutes null when there is no previous machine-status', async () => {
|
|
88
95
|
const { repository } = buildHostMetricsRepository(procDirectory);
|
|
89
96
|
const cacheDir = path.join(dir, 'allIssues-PVT');
|
|
90
97
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
91
|
-
fs.writeFileSync(
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
path.join(cacheDir, 'latest.json'),
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
lastFetchedAt: '2026-06-26T12:00:00.000Z',
|
|
102
|
+
lastFullFetchAt: '2026-06-26T11:00:00.000Z',
|
|
103
|
+
project: {},
|
|
104
|
+
issues: [],
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
92
107
|
|
|
93
108
|
await writeMachineStatus({
|
|
94
109
|
dashboardDataDir: dir,
|
|
@@ -97,7 +112,10 @@ describe('writeMachineStatus', () => {
|
|
|
97
112
|
});
|
|
98
113
|
|
|
99
114
|
const written = readJson(path.join(dir, 'machine-status.json'));
|
|
100
|
-
expect(written).toMatchObject({
|
|
115
|
+
expect(written).toMatchObject({
|
|
116
|
+
cycleMinutes: null,
|
|
117
|
+
lastFetchedAt: '2026-06-26T12:00:00.000Z',
|
|
118
|
+
});
|
|
101
119
|
});
|
|
102
120
|
|
|
103
121
|
it('is a no-op when dashboardDataDir is unset', async () => {
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import {
|
|
4
4
|
ProcHostMetricsRepository,
|
|
5
|
-
|
|
5
|
+
cycleMinutesFromFetchTimestamps,
|
|
6
6
|
} from '../../repositories/ProcHostMetricsRepository';
|
|
7
7
|
|
|
8
8
|
export type MachineStatusWriterParams = {
|
|
@@ -18,6 +18,7 @@ export type MachineStatusFile = {
|
|
|
18
18
|
diskPct: number;
|
|
19
19
|
load: [number, number, number];
|
|
20
20
|
cycleMinutes: number | null;
|
|
21
|
+
lastFetchedAt: string | null;
|
|
21
22
|
capturedAt: string;
|
|
22
23
|
};
|
|
23
24
|
|
|
@@ -29,24 +30,26 @@ const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
|
29
30
|
fs.renameSync(tmpPath, filePath);
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
34
|
+
typeof value === 'object' && value !== null;
|
|
35
|
+
|
|
36
|
+
const readLastFetchedAtFromJsonFile = (filePath: string): string | null => {
|
|
37
|
+
let raw: string;
|
|
34
38
|
try {
|
|
35
|
-
|
|
39
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
36
40
|
} catch {
|
|
37
|
-
return
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
let parsed: unknown;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (isRecord(parsed) && typeof parsed.lastFetchedAt === 'string') {
|
|
50
|
+
return parsed.lastFetchedAt;
|
|
38
51
|
}
|
|
39
|
-
return
|
|
40
|
-
.filter((entry) => entry.endsWith('.json'))
|
|
41
|
-
.map((entry) => {
|
|
42
|
-
try {
|
|
43
|
-
return fs.statSync(path.join(allIssuesCacheDir, entry)).mtimeMs / 1000;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
.filter((value): value is number => value !== null)
|
|
49
|
-
.sort((a, b) => b - a);
|
|
52
|
+
return null;
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
export const writeMachineStatus = async (
|
|
@@ -65,9 +68,16 @@ export const writeMachineStatus = async (
|
|
|
65
68
|
const diskPct = hostMetricsRepository.readDiskUsedPercent();
|
|
66
69
|
const load = hostMetricsRepository.readLoadAverages();
|
|
67
70
|
|
|
68
|
-
const
|
|
69
|
-
|
|
71
|
+
const machineStatusPath = path.join(dashboardDataDir, 'machine-status.json');
|
|
72
|
+
const previousLastFetchedAt =
|
|
73
|
+
readLastFetchedAtFromJsonFile(machineStatusPath);
|
|
74
|
+
const currentLastFetchedAt = allIssuesCacheDir
|
|
75
|
+
? readLastFetchedAtFromJsonFile(path.join(allIssuesCacheDir, 'latest.json'))
|
|
70
76
|
: null;
|
|
77
|
+
const cycleMinutes = cycleMinutesFromFetchTimestamps(
|
|
78
|
+
previousLastFetchedAt,
|
|
79
|
+
currentLastFetchedAt,
|
|
80
|
+
);
|
|
71
81
|
|
|
72
82
|
const file: MachineStatusFile = {
|
|
73
83
|
memPct,
|
|
@@ -75,8 +85,9 @@ export const writeMachineStatus = async (
|
|
|
75
85
|
diskPct,
|
|
76
86
|
load: [load.oneMinute, load.fiveMinute, load.fifteenMinute],
|
|
77
87
|
cycleMinutes,
|
|
88
|
+
lastFetchedAt: currentLastFetchedAt,
|
|
78
89
|
capturedAt: (params.now ?? new Date()).toISOString(),
|
|
79
90
|
};
|
|
80
91
|
|
|
81
|
-
writeJsonAtomic(
|
|
92
|
+
writeJsonAtomic(machineStatusPath, file);
|
|
82
93
|
};
|
|
@@ -56,7 +56,6 @@ const baseParams = {
|
|
|
56
56
|
config: {
|
|
57
57
|
maximumPreparingIssuesCount: 6,
|
|
58
58
|
utilizationPercentageThreshold: 90,
|
|
59
|
-
allowIssueCacheMinutes: 5,
|
|
60
59
|
thresholdForAutoReject: 3,
|
|
61
60
|
},
|
|
62
61
|
};
|
|
@@ -355,10 +354,6 @@ describe('writeSituationFile', () => {
|
|
|
355
354
|
expect.any(String),
|
|
356
355
|
expect.stringContaining('"utilizationPercentageThreshold":90'),
|
|
357
356
|
);
|
|
358
|
-
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
359
|
-
expect.any(String),
|
|
360
|
-
expect.stringContaining('"allowIssueCacheMinutes":5'),
|
|
361
|
-
);
|
|
362
357
|
expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
|
|
363
358
|
expect.any(String),
|
|
364
359
|
expect.stringContaining('"thresholdForAutoReject":3'),
|
|
@@ -15,7 +15,6 @@ export type SituationFileParams = {
|
|
|
15
15
|
config: {
|
|
16
16
|
maximumPreparingIssuesCount: number | null;
|
|
17
17
|
utilizationPercentageThreshold: number;
|
|
18
|
-
allowIssueCacheMinutes: number;
|
|
19
18
|
thresholdForAutoReject: number;
|
|
20
19
|
};
|
|
21
20
|
preparationProcessCheckCommand?: string | null;
|
|
@@ -7,8 +7,6 @@ import { cleanStaleTmuxSessions } from './staleTmuxSessionCleaner';
|
|
|
7
7
|
|
|
8
8
|
const NOW = new Date('2026-06-26T00:00:00.000Z');
|
|
9
9
|
const NOW_EPOCH_SECONDS = Math.floor(NOW.getTime() / 1000);
|
|
10
|
-
const ALLOW_CACHE_MINUTES = 10;
|
|
11
|
-
|
|
12
10
|
const makeProject = (): Project => ({
|
|
13
11
|
id: 'project-1',
|
|
14
12
|
url: 'https://github.com/users/user/projects/1',
|
|
@@ -91,7 +89,6 @@ describe('cleanStaleTmuxSessions', () => {
|
|
|
91
89
|
|
|
92
90
|
await cleanStaleTmuxSessions({
|
|
93
91
|
project: makeProject(),
|
|
94
|
-
allowCacheMinutes: ALLOW_CACHE_MINUTES,
|
|
95
92
|
issueRepository: createMockIssueRepository([
|
|
96
93
|
makeIssue({ status: 'In Progress' }),
|
|
97
94
|
]),
|
|
@@ -124,7 +121,6 @@ describe('cleanStaleTmuxSessions', () => {
|
|
|
124
121
|
|
|
125
122
|
await cleanStaleTmuxSessions({
|
|
126
123
|
project: makeProject(),
|
|
127
|
-
allowCacheMinutes: ALLOW_CACHE_MINUTES,
|
|
128
124
|
issueRepository: createMockIssueRepository([
|
|
129
125
|
makeIssue({ status: IN_TMUX_STATUS_NAME }),
|
|
130
126
|
]),
|
|
@@ -155,7 +151,6 @@ describe('cleanStaleTmuxSessions', () => {
|
|
|
155
151
|
|
|
156
152
|
await cleanStaleTmuxSessions({
|
|
157
153
|
project: makeProject(),
|
|
158
|
-
allowCacheMinutes: ALLOW_CACHE_MINUTES,
|
|
159
154
|
issueRepository: createMockIssueRepository([]),
|
|
160
155
|
localCommandRunner: runner,
|
|
161
156
|
now: NOW,
|
|
@@ -10,7 +10,6 @@ import { NodeTmuxSessionRepository } from '../../repositories/NodeTmuxSessionRep
|
|
|
10
10
|
|
|
11
11
|
export type CleanStaleTmuxSessionsParams = {
|
|
12
12
|
project: Project;
|
|
13
|
-
allowCacheMinutes: number;
|
|
14
13
|
issueRepository: Pick<IssueRepository, 'getAllOpened'>;
|
|
15
14
|
localCommandRunner: LocalCommandRunner;
|
|
16
15
|
now: Date;
|
|
@@ -19,20 +18,13 @@ export type CleanStaleTmuxSessionsParams = {
|
|
|
19
18
|
export const cleanStaleTmuxSessions = async (
|
|
20
19
|
params: CleanStaleTmuxSessionsParams,
|
|
21
20
|
): Promise<void> => {
|
|
22
|
-
const {
|
|
23
|
-
project,
|
|
24
|
-
allowCacheMinutes,
|
|
25
|
-
issueRepository,
|
|
26
|
-
localCommandRunner,
|
|
27
|
-
now,
|
|
28
|
-
} = params;
|
|
21
|
+
const { project, issueRepository, localCommandRunner, now } = params;
|
|
29
22
|
const useCase = new StaleTmuxSessionKillUseCase(
|
|
30
23
|
issueRepository,
|
|
31
24
|
new NodeTmuxSessionRepository(localCommandRunner),
|
|
32
25
|
);
|
|
33
26
|
await useCase.run({
|
|
34
27
|
project,
|
|
35
|
-
allowCacheMinutes,
|
|
36
28
|
excludedStatus: DEFAULT_EXCLUDED_STATUS,
|
|
37
29
|
idleThresholdSeconds: DEFAULT_IDLE_THRESHOLD_SECONDS,
|
|
38
30
|
now,
|
|
@@ -177,4 +177,79 @@ describe('LocalStorageCacheRepository', () => {
|
|
|
177
177
|
expect(callOrder).toEqual(['write', 'rename']);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
describe('getSingle', () => {
|
|
182
|
+
test('returns null when latest.json does not exist', async () => {
|
|
183
|
+
localStorageRepository.listFiles.mockReturnValue([]);
|
|
184
|
+
|
|
185
|
+
const result = await repository.getSingle('test-key');
|
|
186
|
+
|
|
187
|
+
expect(result).toBeNull();
|
|
188
|
+
expect(localStorageRepository.read).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('returns null when the file content is empty', async () => {
|
|
192
|
+
localStorageRepository.listFiles.mockReturnValue(['latest.json']);
|
|
193
|
+
localStorageRepository.read.mockReturnValue(null);
|
|
194
|
+
|
|
195
|
+
const result = await repository.getSingle('test-key');
|
|
196
|
+
|
|
197
|
+
expect(result).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('returns null when the file content is invalid JSON', async () => {
|
|
201
|
+
localStorageRepository.listFiles.mockReturnValue(['latest.json']);
|
|
202
|
+
localStorageRepository.read.mockReturnValue('not-json');
|
|
203
|
+
|
|
204
|
+
const result = await repository.getSingle('test-key');
|
|
205
|
+
|
|
206
|
+
expect(result).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('returns the parsed value from latest.json', async () => {
|
|
210
|
+
localStorageRepository.listFiles.mockReturnValue(['latest.json']);
|
|
211
|
+
localStorageRepository.read.mockReturnValue('{"lastFetchedAt":"x"}');
|
|
212
|
+
|
|
213
|
+
const result = await repository.getSingle('test-key');
|
|
214
|
+
|
|
215
|
+
expect(result).toEqual({ lastFetchedAt: 'x' });
|
|
216
|
+
expect(localStorageRepository.read).toHaveBeenCalledWith(
|
|
217
|
+
'./tmp/cache/test-key/latest.json',
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('setSingle', () => {
|
|
223
|
+
test('atomically writes a temp file then renames it to latest.json', async () => {
|
|
224
|
+
await repository.setSingle('test-key', { test: 'value' });
|
|
225
|
+
|
|
226
|
+
expect(localStorageRepository.mkdir).toHaveBeenCalledWith(
|
|
227
|
+
'./tmp/cache/test-key',
|
|
228
|
+
);
|
|
229
|
+
const writeArgs = localStorageRepository.write.mock.calls[0];
|
|
230
|
+
const writtenPath = writeArgs[0];
|
|
231
|
+
const finalPath = './tmp/cache/test-key/latest.json';
|
|
232
|
+
expect(writtenPath.startsWith(`${finalPath}.`)).toBe(true);
|
|
233
|
+
expect(writtenPath.endsWith('.tmp')).toBe(true);
|
|
234
|
+
expect(writeArgs[1]).toBe('{"test":"value"}');
|
|
235
|
+
expect(localStorageRepository.rename).toHaveBeenCalledWith(
|
|
236
|
+
writtenPath,
|
|
237
|
+
finalPath,
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('writes to the temp file before renaming it into place', async () => {
|
|
242
|
+
const callOrder: string[] = [];
|
|
243
|
+
localStorageRepository.write.mockImplementation(() => {
|
|
244
|
+
callOrder.push('write');
|
|
245
|
+
});
|
|
246
|
+
localStorageRepository.rename.mockImplementation(() => {
|
|
247
|
+
callOrder.push('rename');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await repository.setSingle('ordering-key', { ordered: true });
|
|
251
|
+
|
|
252
|
+
expect(callOrder).toEqual(['write', 'rename']);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
180
255
|
});
|
|
@@ -51,4 +51,28 @@ export class LocalStorageCacheRepository {
|
|
|
51
51
|
this.localStorageRepository.write(tmpPath, JSON.stringify(value));
|
|
52
52
|
this.localStorageRepository.rename(tmpPath, finalPath);
|
|
53
53
|
};
|
|
54
|
+
getSingle = async (key: string): Promise<unknown> => {
|
|
55
|
+
const dirPath = `${this.cachePath}/${key}`;
|
|
56
|
+
const fileName = 'latest.json';
|
|
57
|
+
if (!this.localStorageRepository.listFiles(dirPath).includes(fileName)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const valueStr = this.localStorageRepository.read(`${dirPath}/${fileName}`);
|
|
61
|
+
if (!valueStr) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(valueStr);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
setSingle = async <T>(key: string, value: T): Promise<void> => {
|
|
71
|
+
const dirPath = `${this.cachePath}/${key}`;
|
|
72
|
+
this.localStorageRepository.mkdir(dirPath);
|
|
73
|
+
const finalPath = `${dirPath}/latest.json`;
|
|
74
|
+
const tmpPath = `${finalPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
75
|
+
this.localStorageRepository.write(tmpPath, JSON.stringify(value));
|
|
76
|
+
this.localStorageRepository.rename(tmpPath, finalPath);
|
|
77
|
+
};
|
|
54
78
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
DiskBlocks,
|
|
6
6
|
ProcHostMetricsRepository,
|
|
7
7
|
cpuUsedPercentFromSamples,
|
|
8
|
-
|
|
8
|
+
cycleMinutesFromFetchTimestamps,
|
|
9
9
|
parseCpuSample,
|
|
10
10
|
parseDiskUsedPercent,
|
|
11
11
|
parseLoadAverages,
|
|
@@ -88,18 +88,39 @@ describe('parseDiskUsedPercent', () => {
|
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
describe('
|
|
92
|
-
it('rounds the gap between the
|
|
93
|
-
expect(
|
|
91
|
+
describe('cycleMinutesFromFetchTimestamps', () => {
|
|
92
|
+
it('rounds the gap between the previous and current fetch to whole minutes', () => {
|
|
93
|
+
expect(
|
|
94
|
+
cycleMinutesFromFetchTimestamps(
|
|
95
|
+
'2026-07-07T00:00:00.000Z',
|
|
96
|
+
'2026-07-07T00:14:00.000Z',
|
|
97
|
+
),
|
|
98
|
+
).toBe(14);
|
|
94
99
|
});
|
|
95
100
|
|
|
96
101
|
it('rounds a 1620 second gap to 27 minutes', () => {
|
|
97
|
-
expect(
|
|
102
|
+
expect(
|
|
103
|
+
cycleMinutesFromFetchTimestamps(
|
|
104
|
+
'2026-07-07T00:00:00.000Z',
|
|
105
|
+
'2026-07-07T00:27:00.000Z',
|
|
106
|
+
),
|
|
107
|
+
).toBe(27);
|
|
98
108
|
});
|
|
99
109
|
|
|
100
|
-
it('returns null when
|
|
101
|
-
expect(
|
|
102
|
-
|
|
110
|
+
it('returns null when either timestamp is missing', () => {
|
|
111
|
+
expect(
|
|
112
|
+
cycleMinutesFromFetchTimestamps(null, '2026-07-07T00:00:00.000Z'),
|
|
113
|
+
).toBeNull();
|
|
114
|
+
expect(
|
|
115
|
+
cycleMinutesFromFetchTimestamps('2026-07-07T00:00:00.000Z', null),
|
|
116
|
+
).toBeNull();
|
|
117
|
+
expect(cycleMinutesFromFetchTimestamps(null, null)).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns null when a timestamp is not parseable', () => {
|
|
121
|
+
expect(
|
|
122
|
+
cycleMinutesFromFetchTimestamps('not-a-date', '2026-07-07T00:00:00.000Z'),
|
|
123
|
+
).toBeNull();
|
|
103
124
|
});
|
|
104
125
|
});
|
|
105
126
|
|
|
@@ -111,15 +111,19 @@ export const parseLoadAverages = (loadavgText: string): LoadAverages => {
|
|
|
111
111
|
return { oneMinute, fiveMinute, fifteenMinute };
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
export const
|
|
115
|
-
|
|
114
|
+
export const cycleMinutesFromFetchTimestamps = (
|
|
115
|
+
previousFetchedAtIso: string | null,
|
|
116
|
+
currentFetchedAtIso: string | null,
|
|
116
117
|
): number | null => {
|
|
117
|
-
if (
|
|
118
|
+
if (previousFetchedAtIso === null || currentFetchedAtIso === null) {
|
|
118
119
|
return null;
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
121
|
+
const previousMs = new Date(previousFetchedAtIso).getTime();
|
|
122
|
+
const currentMs = new Date(currentFetchedAtIso).getTime();
|
|
123
|
+
if (!Number.isFinite(previousMs) || !Number.isFinite(currentMs)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return Math.round((currentMs - previousMs) / 60000);
|
|
123
127
|
};
|
|
124
128
|
|
|
125
129
|
export class ProcHostMetricsRepository {
|