github-issue-tower-defence-management 1.45.0 → 1.47.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +64 -0
  3. package/bin/adapter/entry-points/cli/index.js +0 -52
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +0 -2
  6. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +20 -15
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/situationFileWriter.js +98 -0
  10. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -0
  11. package/bin/adapter/repositories/BaseGitHubRepository.js +5 -22
  12. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  13. package/bin/adapter/repositories/GraphqlProjectRepository.js +40 -0
  14. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  15. package/package.json +1 -2
  16. package/src/adapter/entry-points/cli/index.test.ts +0 -49
  17. package/src/adapter/entry-points/cli/index.ts +2 -21
  18. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +0 -6
  19. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +0 -2
  20. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +23 -51
  21. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +27 -18
  22. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +417 -0
  23. package/src/adapter/entry-points/handlers/situationFileWriter.ts +168 -0
  24. package/src/adapter/repositories/BaseGitHubRepository.test.ts +3 -48
  25. package/src/adapter/repositories/BaseGitHubRepository.ts +5 -33
  26. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +72 -0
  27. package/src/adapter/repositories/GraphqlProjectRepository.ts +57 -1
  28. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  29. package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
  30. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  31. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +30 -0
  32. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -0
  33. package/types/adapter/repositories/BaseGitHubRepository.d.ts +0 -1
  34. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  35. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +5 -2
  36. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  37. package/bin/adapter/repositories/CheerioProjectRepository.js +0 -45
  38. package/bin/adapter/repositories/CheerioProjectRepository.js.map +0 -1
  39. package/src/adapter/repositories/CheerioProjectRepository.test.ts +0 -122
  40. package/src/adapter/repositories/CheerioProjectRepository.ts +0 -65
  41. package/types/adapter/repositories/CheerioProjectRepository.d.ts +0 -17
  42. package/types/adapter/repositories/CheerioProjectRepository.d.ts.map +0 -1
@@ -0,0 +1,417 @@
1
+ import fs from 'fs';
2
+ import type { Issue } from '../../../domain/entities/Issue';
3
+ import type { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
4
+ import { parseMeminfo, writeSituationFile } from './situationFileWriter';
5
+
6
+ jest.mock('fs');
7
+
8
+ const FIXTURE_MEMINFO = `MemTotal: 16384000 kB
9
+ MemFree: 4096000 kB
10
+ MemAvailable: 8192000 kB
11
+ Buffers: 512000 kB
12
+ Cached: 3584000 kB
13
+ SwapTotal: 4096000 kB
14
+ SwapFree: 3072000 kB
15
+ SwapCached: 0 kB
16
+ `;
17
+
18
+ const createIssue = (overrides: Partial<Issue> = {}): Issue => ({
19
+ nameWithOwner: 'owner/repo',
20
+ number: 1,
21
+ title: 'Test Issue',
22
+ state: 'OPEN',
23
+ status: null,
24
+ story: null,
25
+ nextActionDate: null,
26
+ nextActionHour: null,
27
+ estimationMinutes: null,
28
+ dependedIssueUrls: [],
29
+ completionDate50PercentConfidence: null,
30
+ url: 'https://github.com/owner/repo/issues/1',
31
+ assignees: [],
32
+ labels: [],
33
+ org: 'owner',
34
+ repo: 'repo',
35
+ body: '',
36
+ itemId: 'item-1',
37
+ isPr: false,
38
+ isInProgress: false,
39
+ isClosed: false,
40
+ createdAt: new Date('2025-01-01'),
41
+ author: 'user',
42
+ ...overrides,
43
+ });
44
+
45
+ const baseParams = {
46
+ cachePath: './tmp/cache/test-project',
47
+ projectId: 'PVT_kwHOtest999',
48
+ issues: [],
49
+ statusNames: {
50
+ awaitingQualityCheckStatus: 'Awaiting quality check',
51
+ preparationStatus: 'Preparation',
52
+ awaitingWorkspaceStatus: 'Awaiting workspace',
53
+ },
54
+ config: {
55
+ maximumPreparingIssuesCount: 6,
56
+ utilizationPercentageThreshold: 90,
57
+ allowIssueCacheMinutes: 5,
58
+ thresholdForAutoReject: 3,
59
+ },
60
+ };
61
+
62
+ beforeEach(() => {
63
+ jest.clearAllMocks();
64
+ jest.mocked(fs.readFileSync).mockReturnValue(FIXTURE_MEMINFO);
65
+ jest.mocked(fs.mkdirSync).mockReturnValue(undefined);
66
+ jest.mocked(fs.writeFileSync).mockReturnValue(undefined);
67
+ jest.mocked(fs.renameSync).mockReturnValue(undefined);
68
+ });
69
+
70
+ describe('parseMeminfo', () => {
71
+ it('parses MemTotal, MemAvailable, SwapTotal, SwapFree from /proc/meminfo fixture', () => {
72
+ const result = parseMeminfo(FIXTURE_MEMINFO);
73
+ expect(result.memTotalKb).toBe(16384000);
74
+ expect(result.memAvailableKb).toBe(8192000);
75
+ expect(result.swapTotalKb).toBe(4096000);
76
+ expect(result.swapFreeKb).toBe(3072000);
77
+ });
78
+
79
+ it('returns 0 for missing fields', () => {
80
+ const result = parseMeminfo('MemTotal: 8192000 kB\n');
81
+ expect(result.memAvailableKb).toBe(0);
82
+ expect(result.swapTotalKb).toBe(0);
83
+ expect(result.swapFreeKb).toBe(0);
84
+ });
85
+ });
86
+
87
+ describe('writeSituationFile', () => {
88
+ describe('status counts from issue fixtures', () => {
89
+ it('counts awaitingQualityCheckImmediatelyActionable correctly', async () => {
90
+ const issues = [
91
+ createIssue({
92
+ status: 'Awaiting quality check',
93
+ dependedIssueUrls: [],
94
+ nextActionDate: null,
95
+ nextActionHour: null,
96
+ }),
97
+ createIssue({
98
+ status: 'Awaiting quality check',
99
+ dependedIssueUrls: ['https://github.com/owner/repo/issues/2'],
100
+ nextActionDate: null,
101
+ nextActionHour: null,
102
+ }),
103
+ createIssue({
104
+ status: 'Awaiting quality check',
105
+ dependedIssueUrls: [],
106
+ nextActionDate: new Date('2025-06-01'),
107
+ nextActionHour: null,
108
+ }),
109
+ createIssue({ status: 'Awaiting quality check', nextActionHour: 9 }),
110
+ createIssue({ status: 'Preparation' }),
111
+ ];
112
+
113
+ await writeSituationFile({ ...baseParams, issues });
114
+
115
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
116
+ expect.any(String),
117
+ expect.stringContaining(
118
+ '"awaitingQualityCheckImmediatelyActionable":1',
119
+ ),
120
+ );
121
+ });
122
+
123
+ it('counts preparation total correctly', async () => {
124
+ const issues = [
125
+ createIssue({ status: 'Preparation' }),
126
+ createIssue({ status: 'Preparation' }),
127
+ createIssue({ status: 'Awaiting workspace' }),
128
+ ];
129
+
130
+ await writeSituationFile({ ...baseParams, issues });
131
+
132
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
133
+ expect.any(String),
134
+ expect.stringContaining('"preparation":2'),
135
+ );
136
+ });
137
+
138
+ it('counts awaitingWorkspaceImmediatelyActionable correctly', async () => {
139
+ const issues = [
140
+ createIssue({
141
+ status: 'Awaiting workspace',
142
+ dependedIssueUrls: [],
143
+ nextActionDate: null,
144
+ nextActionHour: null,
145
+ }),
146
+ createIssue({
147
+ status: 'Awaiting workspace',
148
+ dependedIssueUrls: [],
149
+ nextActionDate: null,
150
+ nextActionHour: null,
151
+ }),
152
+ createIssue({
153
+ status: 'Awaiting workspace',
154
+ dependedIssueUrls: ['https://github.com/owner/repo/issues/10'],
155
+ }),
156
+ createIssue({
157
+ status: 'Awaiting workspace',
158
+ dependedIssueUrls: [],
159
+ nextActionDate: new Date('2025-06-01'),
160
+ }),
161
+ ];
162
+
163
+ await writeSituationFile({ ...baseParams, issues });
164
+
165
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
166
+ expect.any(String),
167
+ expect.stringContaining('"awaitingWorkspaceImmediatelyActionable":2'),
168
+ );
169
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
170
+ expect.any(String),
171
+ expect.stringContaining('"awaitingWorkspaceBlockedByDependency":1'),
172
+ );
173
+ });
174
+
175
+ it('sets all counts to 0 when statusNames are null', async () => {
176
+ const issues = [
177
+ createIssue({ status: 'Preparation' }),
178
+ createIssue({ status: 'Awaiting workspace' }),
179
+ ];
180
+ const params = {
181
+ ...baseParams,
182
+ statusNames: {
183
+ awaitingQualityCheckStatus: null,
184
+ preparationStatus: null,
185
+ awaitingWorkspaceStatus: null,
186
+ },
187
+ issues,
188
+ };
189
+
190
+ await writeSituationFile(params);
191
+
192
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
193
+ expect.any(String),
194
+ expect.stringContaining(
195
+ '"awaitingQualityCheckImmediatelyActionable":0',
196
+ ),
197
+ );
198
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
199
+ expect.any(String),
200
+ expect.stringContaining('"preparation":0'),
201
+ );
202
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
203
+ expect.any(String),
204
+ expect.stringContaining('"awaitingWorkspaceImmediatelyActionable":0'),
205
+ );
206
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
207
+ expect.any(String),
208
+ expect.stringContaining('"awaitingWorkspaceBlockedByDependency":0'),
209
+ );
210
+ });
211
+ });
212
+
213
+ describe('system metrics from /proc/meminfo fixture', () => {
214
+ it('parses memory usedPercent from fixture', async () => {
215
+ await writeSituationFile(baseParams);
216
+
217
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
218
+ expect.any(String),
219
+ expect.stringContaining('"usedPercent":50'),
220
+ );
221
+ });
222
+
223
+ it('parses swap usedPercent from fixture', async () => {
224
+ await writeSituationFile(baseParams);
225
+
226
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
227
+ expect.any(String),
228
+ expect.stringContaining('"usedPercent":25'),
229
+ );
230
+ });
231
+
232
+ it('includes memory totalGib from fixture', async () => {
233
+ await writeSituationFile(baseParams);
234
+
235
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
236
+ expect.any(String),
237
+ expect.stringContaining('"system"'),
238
+ );
239
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
240
+ expect.any(String),
241
+ expect.stringContaining('"memory"'),
242
+ );
243
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
244
+ expect.any(String),
245
+ expect.stringContaining('"swap"'),
246
+ );
247
+ });
248
+
249
+ it('handles zero swap total without division by zero', async () => {
250
+ jest
251
+ .mocked(fs.readFileSync)
252
+ .mockReturnValue(
253
+ 'MemTotal: 8192000 kB\nMemAvailable: 4096000 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\n',
254
+ );
255
+
256
+ await writeSituationFile(baseParams);
257
+
258
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
259
+ expect.any(String),
260
+ expect.stringContaining(
261
+ '"swap":{"usedPercent":0,"usedGib":0,"totalGib":0}',
262
+ ),
263
+ );
264
+ });
265
+ });
266
+
267
+ describe('atomic write path', () => {
268
+ it('writes to tmp file then renames to final path', async () => {
269
+ await writeSituationFile(baseParams);
270
+
271
+ const expectedFinalPath = `${baseParams.cachePath}/situation-${baseParams.projectId}.json`;
272
+ const expectedTmpPath = `${expectedFinalPath}.tmp`;
273
+
274
+ expect(jest.mocked(fs.mkdirSync)).toHaveBeenCalledWith(
275
+ baseParams.cachePath,
276
+ { recursive: true },
277
+ );
278
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
279
+ expectedTmpPath,
280
+ expect.any(String),
281
+ );
282
+ expect(jest.mocked(fs.renameSync)).toHaveBeenCalledWith(
283
+ expectedTmpPath,
284
+ expectedFinalPath,
285
+ );
286
+ });
287
+
288
+ it('writes mkdirSync before writeFileSync before renameSync', async () => {
289
+ const callOrder: string[] = [];
290
+ jest.mocked(fs.mkdirSync).mockImplementation((): undefined => {
291
+ callOrder.push('mkdir');
292
+ return undefined;
293
+ });
294
+ jest.mocked(fs.writeFileSync).mockImplementation((): void => {
295
+ callOrder.push('write');
296
+ });
297
+ jest.mocked(fs.renameSync).mockImplementation((): void => {
298
+ callOrder.push('rename');
299
+ });
300
+
301
+ await writeSituationFile(baseParams);
302
+
303
+ expect(callOrder).toEqual(['mkdir', 'write', 'rename']);
304
+ });
305
+
306
+ it('written JSON contains config and capturedAt fields', async () => {
307
+ await writeSituationFile(baseParams);
308
+
309
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
310
+ expect.any(String),
311
+ expect.stringContaining('"capturedAt"'),
312
+ );
313
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
314
+ expect.any(String),
315
+ expect.stringContaining('"maximumPreparingIssuesCount":6'),
316
+ );
317
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
318
+ expect.any(String),
319
+ expect.stringContaining('"utilizationPercentageThreshold":90'),
320
+ );
321
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
322
+ expect.any(String),
323
+ expect.stringContaining('"allowIssueCacheMinutes":5'),
324
+ );
325
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
326
+ expect.any(String),
327
+ expect.stringContaining('"thresholdForAutoReject":3'),
328
+ );
329
+ });
330
+ });
331
+
332
+ describe('running preparation processes', () => {
333
+ it('counts running processes via preparationProcessCheckCommand', async () => {
334
+ const mockRunCommand = jest
335
+ .fn()
336
+ .mockImplementation(async (_program: string, args: string[]) =>
337
+ Promise.resolve({
338
+ stdout: '',
339
+ stderr: '',
340
+ exitCode:
341
+ args[3] === 'https://github.com/owner/repo/issues/1' ? 0 : 1,
342
+ }),
343
+ );
344
+ const mockRunner: LocalCommandRunner = {
345
+ runCommand: mockRunCommand,
346
+ };
347
+ const issues = [
348
+ createIssue({
349
+ status: 'Preparation',
350
+ url: 'https://github.com/owner/repo/issues/1',
351
+ number: 1,
352
+ }),
353
+ createIssue({
354
+ status: 'Preparation',
355
+ url: 'https://github.com/owner/repo/issues/2',
356
+ number: 2,
357
+ }),
358
+ ];
359
+
360
+ await writeSituationFile({
361
+ ...baseParams,
362
+ issues,
363
+ preparationProcessCheckCommand: 'pgrep -fa "{URL}"',
364
+ localCommandRunner: mockRunner,
365
+ });
366
+
367
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
368
+ expect.any(String),
369
+ expect.stringContaining('"runningPreparation":1'),
370
+ );
371
+ });
372
+
373
+ it('sets runningPreparation to 0 when no preparation issues exist', async () => {
374
+ const mockRunCommand = jest.fn();
375
+ const mockRunner: LocalCommandRunner = {
376
+ runCommand: mockRunCommand,
377
+ };
378
+
379
+ await writeSituationFile({
380
+ ...baseParams,
381
+ issues: [],
382
+ preparationProcessCheckCommand: 'pgrep -fa "{URL}"',
383
+ localCommandRunner: mockRunner,
384
+ });
385
+
386
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
387
+ expect.any(String),
388
+ expect.stringContaining('"runningPreparation":0'),
389
+ );
390
+ expect(mockRunCommand).not.toHaveBeenCalled();
391
+ });
392
+
393
+ it('sets runningPreparation to null when no localCommandRunner provided', async () => {
394
+ await writeSituationFile({
395
+ ...baseParams,
396
+ preparationProcessCheckCommand: 'pgrep -fa "{URL}"',
397
+ });
398
+
399
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
400
+ expect.any(String),
401
+ expect.stringContaining('"runningPreparation":null'),
402
+ );
403
+ });
404
+
405
+ it('sets runningPreparation to null when no preparationProcessCheckCommand', async () => {
406
+ await writeSituationFile({
407
+ ...baseParams,
408
+ preparationProcessCheckCommand: null,
409
+ });
410
+
411
+ expect(jest.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
412
+ expect.any(String),
413
+ expect.stringContaining('"runningPreparation":null'),
414
+ );
415
+ });
416
+ });
417
+ });
@@ -0,0 +1,168 @@
1
+ import fs from 'fs';
2
+ import type { Issue } from '../../../domain/entities/Issue';
3
+ import type { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
4
+
5
+ export type SituationFileParams = {
6
+ cachePath: string;
7
+ projectId: string;
8
+ issues: Issue[];
9
+ statusNames: {
10
+ awaitingQualityCheckStatus: string | null;
11
+ preparationStatus: string | null;
12
+ awaitingWorkspaceStatus: string | null;
13
+ };
14
+ config: {
15
+ maximumPreparingIssuesCount: number | null;
16
+ utilizationPercentageThreshold: number;
17
+ allowIssueCacheMinutes: number;
18
+ thresholdForAutoReject: number;
19
+ };
20
+ preparationProcessCheckCommand?: string | null;
21
+ localCommandRunner?: LocalCommandRunner;
22
+ };
23
+
24
+ type MeminfoValues = {
25
+ memTotalKb: number;
26
+ memAvailableKb: number;
27
+ swapTotalKb: number;
28
+ swapFreeKb: number;
29
+ };
30
+
31
+ export const parseMeminfo = (meminfo: string): MeminfoValues => {
32
+ const getValue = (key: string): number => {
33
+ const match = meminfo.match(new RegExp(`^${key}:\\s+(\\d+)`, 'm'));
34
+ return match ? parseInt(match[1], 10) : 0;
35
+ };
36
+ return {
37
+ memTotalKb: getValue('MemTotal'),
38
+ memAvailableKb: getValue('MemAvailable'),
39
+ swapTotalKb: getValue('SwapTotal'),
40
+ swapFreeKb: getValue('SwapFree'),
41
+ };
42
+ };
43
+
44
+ const kbToGib = (kb: number): number =>
45
+ Math.round((kb / 1024 / 1024) * 100) / 100;
46
+
47
+ const toPercent = (used: number, total: number): number =>
48
+ total > 0 ? Math.round((used / total) * 1000) / 10 : 0;
49
+
50
+ const isImmediatelyActionable = (issue: Issue): boolean =>
51
+ issue.dependedIssueUrls.length === 0 &&
52
+ issue.nextActionDate === null &&
53
+ issue.nextActionHour === null;
54
+
55
+ const countRunningProcesses = async (
56
+ preparationIssues: Issue[],
57
+ commandTemplate: string,
58
+ localCommandRunner: LocalCommandRunner,
59
+ ): Promise<number> => {
60
+ const resolvedTemplate = commandTemplate.replace('{URL}', '$1');
61
+ const checks = await Promise.all(
62
+ preparationIssues.map(async (issue) => {
63
+ const { exitCode } = await localCommandRunner.runCommand('sh', [
64
+ '-c',
65
+ resolvedTemplate,
66
+ '--',
67
+ issue.url,
68
+ ]);
69
+ return exitCode === 0;
70
+ }),
71
+ );
72
+ return checks.filter(Boolean).length;
73
+ };
74
+
75
+ export const writeSituationFile = async (
76
+ params: SituationFileParams,
77
+ ): Promise<void> => {
78
+ const {
79
+ cachePath,
80
+ projectId,
81
+ issues,
82
+ statusNames,
83
+ config,
84
+ preparationProcessCheckCommand,
85
+ localCommandRunner,
86
+ } = params;
87
+
88
+ const awaitingQualityCheckImmediatelyActionable =
89
+ statusNames.awaitingQualityCheckStatus !== null
90
+ ? issues.filter(
91
+ (i) =>
92
+ i.status === statusNames.awaitingQualityCheckStatus &&
93
+ isImmediatelyActionable(i),
94
+ ).length
95
+ : 0;
96
+
97
+ const preparationIssues =
98
+ statusNames.preparationStatus !== null
99
+ ? issues.filter((i) => i.status === statusNames.preparationStatus)
100
+ : [];
101
+
102
+ const awaitingWorkspaceIssues =
103
+ statusNames.awaitingWorkspaceStatus !== null
104
+ ? issues.filter((i) => i.status === statusNames.awaitingWorkspaceStatus)
105
+ : [];
106
+
107
+ const awaitingWorkspaceImmediatelyActionable = awaitingWorkspaceIssues.filter(
108
+ isImmediatelyActionable,
109
+ ).length;
110
+
111
+ const awaitingWorkspaceBlockedByDependency = awaitingWorkspaceIssues.filter(
112
+ (i) => i.dependedIssueUrls.length > 0,
113
+ ).length;
114
+
115
+ let runningPreparation: number | null = null;
116
+ if (
117
+ preparationProcessCheckCommand &&
118
+ localCommandRunner &&
119
+ preparationIssues.length > 0
120
+ ) {
121
+ runningPreparation = await countRunningProcesses(
122
+ preparationIssues,
123
+ preparationProcessCheckCommand,
124
+ localCommandRunner,
125
+ );
126
+ } else if (preparationProcessCheckCommand && localCommandRunner) {
127
+ runningPreparation = 0;
128
+ }
129
+
130
+ const meminfo = fs.readFileSync('/proc/meminfo', 'utf-8');
131
+ const { memTotalKb, memAvailableKb, swapTotalKb, swapFreeKb } =
132
+ parseMeminfo(meminfo);
133
+
134
+ const memUsedKb = memTotalKb - memAvailableKb;
135
+ const swapUsedKb = swapTotalKb - swapFreeKb;
136
+
137
+ const situation = {
138
+ capturedAt: new Date().toISOString(),
139
+ config,
140
+ status: {
141
+ awaitingQualityCheckImmediatelyActionable,
142
+ preparation: preparationIssues.length,
143
+ awaitingWorkspaceImmediatelyActionable,
144
+ awaitingWorkspaceBlockedByDependency,
145
+ },
146
+ processes: {
147
+ runningPreparation,
148
+ },
149
+ system: {
150
+ memory: {
151
+ usedPercent: toPercent(memUsedKb, memTotalKb),
152
+ usedGib: kbToGib(memUsedKb),
153
+ totalGib: kbToGib(memTotalKb),
154
+ },
155
+ swap: {
156
+ usedPercent: toPercent(swapUsedKb, swapTotalKb),
157
+ usedGib: kbToGib(swapUsedKb),
158
+ totalGib: kbToGib(swapTotalKb),
159
+ },
160
+ },
161
+ };
162
+
163
+ const finalPath = `${cachePath}/situation-${projectId}.json`;
164
+ const tmpPath = `${finalPath}.tmp`;
165
+ fs.mkdirSync(cachePath, { recursive: true });
166
+ fs.writeFileSync(tmpPath, JSON.stringify(situation));
167
+ fs.renameSync(tmpPath, finalPath);
168
+ };
@@ -19,12 +19,6 @@ import fs from 'fs';
19
19
  import { BaseGitHubRepository } from './BaseGitHubRepository';
20
20
  import resetAllMocks = jest.resetAllMocks;
21
21
  import { LocalStorageRepository } from './LocalStorageRepository';
22
-
23
- const mockGetCookieContent = jest.fn<Promise<unknown>, unknown[]>();
24
- jest.mock('gh-cookie', () => ({
25
- getCookieContent: (...args: unknown[]): Promise<unknown> =>
26
- mockGetCookieContent(...args),
27
- }));
28
22
  describe('BaseGitHubRepository', () => {
29
23
  const jsonFilePath = './tmp/github.com.cookies.json';
30
24
  const localStorageRepository = new LocalStorageRepository();
@@ -133,7 +127,6 @@ describe('BaseGitHubRepository', () => {
133
127
  'dummy-password',
134
128
  'dummy-authenticator-key',
135
129
  );
136
- this.cookieRefreshRetryDelayMs = 0;
137
130
  }
138
131
  }
139
132
 
@@ -153,8 +146,6 @@ describe('BaseGitHubRepository', () => {
153
146
  beforeEach(() => {
154
147
  mockKyGet.mockReset().mockReturnValue({ text: mockKyGetText });
155
148
  mockKyGetText.mockReset();
156
- mockGetCookieContent.mockReset();
157
- mockGetCookieContent.mockResolvedValue(validCookieJson);
158
149
  fs.writeFileSync(refreshCookieJsonFilePath, validCookieJson);
159
150
  });
160
151
 
@@ -182,10 +173,7 @@ describe('BaseGitHubRepository', () => {
182
173
  it('should fail when HTML contains username in content but not in user-login meta tag (not logged in)', async () => {
183
174
  const repository = new RefreshTestRepository();
184
175
  const notLoggedInHtml = `<html><head><meta name="user-login" content=""></head><body><h1>${ghUserName}</h1><p>Public profile</p></body></html>`;
185
- mockKyGetText
186
- .mockResolvedValueOnce(notLoggedInHtml)
187
- .mockResolvedValueOnce(notLoggedInHtml)
188
- .mockResolvedValueOnce(notLoggedInHtml);
176
+ mockKyGetText.mockResolvedValueOnce(notLoggedInHtml);
189
177
 
190
178
  await expect(repository.refreshCookie()).rejects.toThrow(
191
179
  'Failed to refresh cookie',
@@ -210,47 +198,14 @@ describe('BaseGitHubRepository', () => {
210
198
  );
211
199
  });
212
200
 
213
- it('should throw when all three attempts fail', async () => {
201
+ it('should throw when the authentication check fails', async () => {
214
202
  const repository = new RefreshTestRepository();
215
203
  const notLoggedInHtml = `<html><head><meta name="user-login" content=""></head><body></body></html>`;
216
- mockKyGetText
217
- .mockResolvedValueOnce(notLoggedInHtml)
218
- .mockResolvedValueOnce(notLoggedInHtml)
219
- .mockResolvedValueOnce(notLoggedInHtml);
204
+ mockKyGetText.mockResolvedValueOnce(notLoggedInHtml);
220
205
 
221
206
  await expect(repository.refreshCookie()).rejects.toThrow(
222
207
  'Failed to refresh cookie',
223
208
  );
224
209
  });
225
-
226
- it('should reset cookie cache before regenerating so new cookie is used', async () => {
227
- const repository = new RefreshTestRepository();
228
- mockKyGetText
229
- .mockResolvedValueOnce(
230
- `<html><head><meta name="user-login" content=""></head><body></body></html>`,
231
- )
232
- .mockResolvedValueOnce(
233
- `<html><head><meta name="user-login" content="${ghUserName}"></head><body></body></html>`,
234
- );
235
-
236
- await expect(repository.refreshCookie()).resolves.toBeUndefined();
237
- expect(mockKyGet).toHaveBeenCalledTimes(2);
238
- expect(mockGetCookieContent).toHaveBeenCalledTimes(1);
239
- });
240
-
241
- it('should succeed on third attempt after two failed cookie refresh attempts', async () => {
242
- const repository = new RefreshTestRepository();
243
- const notLoggedInHtml = `<html><head><meta name="user-login" content=""></head><body></body></html>`;
244
- mockKyGetText
245
- .mockResolvedValueOnce(notLoggedInHtml)
246
- .mockResolvedValueOnce(notLoggedInHtml)
247
- .mockResolvedValueOnce(
248
- `<html><head><meta name="user-login" content="${ghUserName}"></head><body></body></html>`,
249
- );
250
-
251
- await expect(repository.refreshCookie()).resolves.toBeUndefined();
252
- expect(mockKyGet).toHaveBeenCalledTimes(3);
253
- expect(mockGetCookieContent).toHaveBeenCalledTimes(2);
254
- });
255
210
  });
256
211
  });