github-issue-tower-defence-management 1.148.24 → 1.149.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/.github/workflows/publish.yml +40 -7
- package/.prettierignore +1 -0
- package/CHANGELOG.md +20 -0
- package/README.md +4 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +1 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js +126 -0
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js.map +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +5 -3
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +6 -2
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/entities/KevReportWatermark.js +3 -0
- package/bin/domain/entities/KevReportWatermark.js.map +1 -0
- package/bin/domain/usecases/DailySecurityScanUseCase.js +56 -7
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +9 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +12 -11
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +19 -0
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js.map +1 -0
- package/package.json +1 -1
- package/scripts/defaultBranchTipVerify.sh +23 -0
- package/scripts/testWorkflowRunVerify.sh +47 -0
- package/src/adapter/ci/publishTestWorkflowGate.test.ts +653 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +35 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.test.ts +253 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.ts +130 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +2 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +3 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +159 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +7 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +108 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +10 -2
- package/src/domain/entities/KevReportWatermark.ts +4 -0
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +210 -0
- package/src/domain/usecases/DailySecurityScanUseCase.ts +84 -7
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +58 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +21 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +28 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +146 -8
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +19 -10
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +212 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +32 -0
- package/src/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.ts +11 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts.map +1 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/KevReportWatermark.d.ts +5 -0
- package/types/domain/entities/KevReportWatermark.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts +3 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +3 -2
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts +15 -0
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -10,6 +10,9 @@ const createFallback = (): Mocked<SilentSessionMessageComposer> => ({
|
|
|
10
10
|
.fn()
|
|
11
11
|
.mockReturnValue('FALLBACK_STALE_OWNER_CALL'),
|
|
12
12
|
composeSubAgentSection: jest.fn().mockReturnValue('FALLBACK_SUB'),
|
|
13
|
+
composeSubAgentUnconsumedResultSection: jest
|
|
14
|
+
.fn()
|
|
15
|
+
.mockReturnValue('FALLBACK_UNCONSUMED_RESULT'),
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
const noTemplates = {
|
|
@@ -142,6 +145,7 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
142
145
|
silentSeconds: 360,
|
|
143
146
|
runningSeconds: 1200,
|
|
144
147
|
waitingOnExternalProcess: false,
|
|
148
|
+
finishedResultUnconsumed: false,
|
|
145
149
|
};
|
|
146
150
|
expect(
|
|
147
151
|
composer.composeSubAgentSection({
|
|
@@ -172,6 +176,7 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
172
176
|
silentSeconds: 360,
|
|
173
177
|
runningSeconds: 60,
|
|
174
178
|
waitingOnExternalProcess: false,
|
|
179
|
+
finishedResultUnconsumed: false,
|
|
175
180
|
},
|
|
176
181
|
],
|
|
177
182
|
longRunningSubAgents: [],
|
|
@@ -202,6 +207,7 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
202
207
|
silentSeconds: 30,
|
|
203
208
|
runningSeconds: 1200,
|
|
204
209
|
waitingOnExternalProcess: false,
|
|
210
|
+
finishedResultUnconsumed: false,
|
|
205
211
|
},
|
|
206
212
|
],
|
|
207
213
|
});
|
|
@@ -228,6 +234,7 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
228
234
|
silentSeconds: 360,
|
|
229
235
|
runningSeconds: 1200,
|
|
230
236
|
waitingOnExternalProcess: false,
|
|
237
|
+
finishedResultUnconsumed: false,
|
|
231
238
|
};
|
|
232
239
|
const section = composer.composeSubAgentSection({
|
|
233
240
|
idleSubAgents: [subAgent],
|
|
@@ -257,6 +264,7 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
257
264
|
silentSeconds: 360,
|
|
258
265
|
runningSeconds: 60,
|
|
259
266
|
waitingOnExternalProcess: false,
|
|
267
|
+
finishedResultUnconsumed: false,
|
|
260
268
|
};
|
|
261
269
|
const section = composer.composeSubAgentSection({
|
|
262
270
|
idleSubAgents: [idleSubAgent],
|
|
@@ -269,6 +277,33 @@ describe('ConfigurableSilentSessionMessageComposer', () => {
|
|
|
269
277
|
});
|
|
270
278
|
});
|
|
271
279
|
|
|
280
|
+
it('delegates the unconsumed-result section to the fallback composer because no template governs it', () => {
|
|
281
|
+
const fallback = createFallback();
|
|
282
|
+
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
283
|
+
{
|
|
284
|
+
...noTemplates,
|
|
285
|
+
subAgentIdleMessageHeader: 'CUSTOM_IDLE_HEADER',
|
|
286
|
+
},
|
|
287
|
+
fallback,
|
|
288
|
+
);
|
|
289
|
+
const unconsumedResultSubAgent = {
|
|
290
|
+
label: 'agent-aaabbbbcccc30001',
|
|
291
|
+
silentSeconds: 5220,
|
|
292
|
+
runningSeconds: 5400,
|
|
293
|
+
waitingOnExternalProcess: false,
|
|
294
|
+
finishedResultUnconsumed: true,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const section = composer.composeSubAgentUnconsumedResultSection([
|
|
298
|
+
unconsumedResultSubAgent,
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
expect(section).toBe('FALLBACK_UNCONSUMED_RESULT');
|
|
302
|
+
expect(
|
|
303
|
+
fallback.composeSubAgentUnconsumedResultSection,
|
|
304
|
+
).toHaveBeenCalledWith([unconsumedResultSubAgent]);
|
|
305
|
+
});
|
|
306
|
+
|
|
272
307
|
it('does not double-prepend the sentinel when the template already carries it', () => {
|
|
273
308
|
const fallback = createFallback();
|
|
274
309
|
const composer = new ConfigurableSilentSessionMessageComposer(
|
|
@@ -99,6 +99,13 @@ export class ConfigurableSilentSessionMessageComposer implements SilentSessionMe
|
|
|
99
99
|
return withReminderSentinel(sections.join('\n\n'));
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
+
composeSubAgentUnconsumedResultSection = (
|
|
103
|
+
unconsumedResultSubAgents: SubAgentActivity[],
|
|
104
|
+
): string =>
|
|
105
|
+
this.fallback.composeSubAgentUnconsumedResultSection(
|
|
106
|
+
unconsumedResultSubAgents,
|
|
107
|
+
);
|
|
108
|
+
|
|
102
109
|
private composeIdleSection = (
|
|
103
110
|
idleSubAgents: SubAgentActivity[],
|
|
104
111
|
header: string | null,
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { FileSystemKevReportWatermarkRepository } from './FileSystemKevReportWatermarkRepository';
|
|
5
|
+
|
|
6
|
+
describe('FileSystemKevReportWatermarkRepository', () => {
|
|
7
|
+
let temporaryDirectory: string;
|
|
8
|
+
let stateFilePath: string;
|
|
9
|
+
let errorLog: jest.SpyInstance;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
temporaryDirectory = fs.mkdtempSync(
|
|
13
|
+
path.join(os.tmpdir(), 'kev-report-watermark-'),
|
|
14
|
+
);
|
|
15
|
+
stateFilePath = path.join(
|
|
16
|
+
temporaryDirectory,
|
|
17
|
+
'nested',
|
|
18
|
+
'kev-report-watermark.json',
|
|
19
|
+
);
|
|
20
|
+
errorLog = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
errorLog.mockRestore();
|
|
25
|
+
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const writeStoredContent = (content: string): void => {
|
|
29
|
+
fs.mkdirSync(path.dirname(stateFilePath), { recursive: true });
|
|
30
|
+
fs.writeFileSync(stateFilePath, content);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const loggedMessages = (): string[] =>
|
|
34
|
+
errorLog.mock.calls.map((call: unknown[]) => String(call[0]));
|
|
35
|
+
|
|
36
|
+
it('reports an absent state file as absent without logging an error', async () => {
|
|
37
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
38
|
+
stateFilePath,
|
|
39
|
+
);
|
|
40
|
+
expect(await repository.load()).toEqual({ type: 'absent' });
|
|
41
|
+
expect(loggedMessages()).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('persists and reloads the watermark', async () => {
|
|
45
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
46
|
+
stateFilePath,
|
|
47
|
+
);
|
|
48
|
+
await repository.save({
|
|
49
|
+
lastReportedDateAdded: '2024-01-02',
|
|
50
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002'],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(await repository.load()).toEqual({
|
|
54
|
+
type: 'stored',
|
|
55
|
+
watermark: {
|
|
56
|
+
lastReportedDateAdded: '2024-01-02',
|
|
57
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002'],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
expect(loggedMessages()).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('overwrites a previously saved watermark', async () => {
|
|
64
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
65
|
+
stateFilePath,
|
|
66
|
+
);
|
|
67
|
+
await repository.save({
|
|
68
|
+
lastReportedDateAdded: '2024-01-02',
|
|
69
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002'],
|
|
70
|
+
});
|
|
71
|
+
await repository.save({
|
|
72
|
+
lastReportedDateAdded: '2024-01-03',
|
|
73
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0003'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(await repository.load()).toEqual({
|
|
77
|
+
type: 'stored',
|
|
78
|
+
watermark: {
|
|
79
|
+
lastReportedDateAdded: '2024-01-03',
|
|
80
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0003'],
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reports invalid JSON as unreadable, logging the state file path and the cause, and leaves the file untouched', async () => {
|
|
86
|
+
writeStoredContent('not json');
|
|
87
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
88
|
+
stateFilePath,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const result = await repository.load();
|
|
92
|
+
|
|
93
|
+
expect(result.type).toBe('unreadable');
|
|
94
|
+
expect(result).toHaveProperty('reason');
|
|
95
|
+
expect(
|
|
96
|
+
loggedMessages().some(
|
|
97
|
+
(message) =>
|
|
98
|
+
message.includes(stateFilePath) &&
|
|
99
|
+
message.includes('valid JSON') &&
|
|
100
|
+
message.includes('SyntaxError'),
|
|
101
|
+
),
|
|
102
|
+
).toBe(true);
|
|
103
|
+
expect(fs.readFileSync(stateFilePath, 'utf8')).toBe('not json');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('reports stored content that is not a JSON object as unreadable', async () => {
|
|
107
|
+
writeStoredContent(JSON.stringify(['2024-01-02']));
|
|
108
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
109
|
+
stateFilePath,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
113
|
+
expect(
|
|
114
|
+
loggedMessages().some(
|
|
115
|
+
(message) =>
|
|
116
|
+
message.includes(stateFilePath) && message.includes('JSON object'),
|
|
117
|
+
),
|
|
118
|
+
).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('reports a non-string stored date as unreadable', async () => {
|
|
122
|
+
writeStoredContent(
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
lastReportedDateAdded: 20240102,
|
|
125
|
+
reportedCveIdsOnLastReportedDateAdded: [],
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
129
|
+
stateFilePath,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
133
|
+
expect(
|
|
134
|
+
loggedMessages().some(
|
|
135
|
+
(message) =>
|
|
136
|
+
message.includes(stateFilePath) &&
|
|
137
|
+
message.includes('lastReportedDateAdded'),
|
|
138
|
+
),
|
|
139
|
+
).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('reports an empty stored date as unreadable rather than as a usable boundary', async () => {
|
|
143
|
+
writeStoredContent(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
lastReportedDateAdded: '',
|
|
146
|
+
reportedCveIdsOnLastReportedDateAdded: [],
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
150
|
+
stateFilePath,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
154
|
+
expect(
|
|
155
|
+
loggedMessages().some(
|
|
156
|
+
(message) =>
|
|
157
|
+
message.includes(stateFilePath) &&
|
|
158
|
+
message.includes('calendar date') &&
|
|
159
|
+
message.includes('YYYY-MM-DD'),
|
|
160
|
+
),
|
|
161
|
+
).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('reports a stored date that is not a real calendar date as unreadable', async () => {
|
|
165
|
+
writeStoredContent(
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
lastReportedDateAdded: '2024-02-31',
|
|
168
|
+
reportedCveIdsOnLastReportedDateAdded: [],
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
172
|
+
stateFilePath,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
176
|
+
expect(
|
|
177
|
+
loggedMessages().some((message) => message.includes('calendar date')),
|
|
178
|
+
).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('reports a stored identifier list that is not an array as unreadable', async () => {
|
|
182
|
+
writeStoredContent(
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
lastReportedDateAdded: '2024-01-02',
|
|
185
|
+
reportedCveIdsOnLastReportedDateAdded: 'CVE-2024-0002',
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
189
|
+
stateFilePath,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
193
|
+
expect(
|
|
194
|
+
loggedMessages().some((message) =>
|
|
195
|
+
message.includes('reportedCveIdsOnLastReportedDateAdded'),
|
|
196
|
+
),
|
|
197
|
+
).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('reports stored identifiers that are not all strings as unreadable', async () => {
|
|
201
|
+
writeStoredContent(
|
|
202
|
+
JSON.stringify({
|
|
203
|
+
lastReportedDateAdded: '2024-01-02',
|
|
204
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002', 7],
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
208
|
+
stateFilePath,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
212
|
+
expect(
|
|
213
|
+
loggedMessages().some((message) => message.includes('non-string')),
|
|
214
|
+
).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('reports a path that cannot be read as unreadable rather than as absent', async () => {
|
|
218
|
+
fs.mkdirSync(stateFilePath, { recursive: true });
|
|
219
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
220
|
+
stateFilePath,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect((await repository.load()).type).toBe('unreadable');
|
|
224
|
+
expect(
|
|
225
|
+
loggedMessages().some(
|
|
226
|
+
(message) =>
|
|
227
|
+
message.includes(stateFilePath) &&
|
|
228
|
+
message.includes('could not be read'),
|
|
229
|
+
),
|
|
230
|
+
).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('logs the state file path and the cause when the watermark cannot be written', async () => {
|
|
234
|
+
fs.writeFileSync(path.dirname(stateFilePath), 'not a directory');
|
|
235
|
+
const repository = new FileSystemKevReportWatermarkRepository(
|
|
236
|
+
stateFilePath,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await expect(
|
|
240
|
+
repository.save({
|
|
241
|
+
lastReportedDateAdded: '2024-01-02',
|
|
242
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002'],
|
|
243
|
+
}),
|
|
244
|
+
).rejects.toThrow();
|
|
245
|
+
expect(
|
|
246
|
+
loggedMessages().some(
|
|
247
|
+
(message) =>
|
|
248
|
+
message.includes(stateFilePath) &&
|
|
249
|
+
message.includes('Unable to write the KEV report watermark'),
|
|
250
|
+
),
|
|
251
|
+
).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { KevReportWatermark } from '../../domain/entities/KevReportWatermark';
|
|
5
|
+
import {
|
|
6
|
+
KevReportWatermarkLoadResult,
|
|
7
|
+
KevReportWatermarkRepository,
|
|
8
|
+
} from '../../domain/usecases/adapter-interfaces/KevReportWatermarkRepository';
|
|
9
|
+
|
|
10
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
11
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
|
|
13
|
+
const errorCode = (error: unknown): string | null => {
|
|
14
|
+
if (!isRecord(error)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const code = error.code;
|
|
18
|
+
return typeof code === 'string' ? code : null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const isCalendarYmd = (value: string): boolean => {
|
|
22
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
26
|
+
return (
|
|
27
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
28
|
+
parsed.toISOString().slice(0, 10) === value
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const defaultStateFilePath = (): string => {
|
|
33
|
+
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
34
|
+
return path.join(base, 'tdpm', 'kev-report-watermark.json');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class FileSystemKevReportWatermarkRepository implements KevReportWatermarkRepository {
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly stateFilePath: string = defaultStateFilePath(),
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
load = async (): Promise<KevReportWatermarkLoadResult> => {
|
|
43
|
+
let raw: string;
|
|
44
|
+
try {
|
|
45
|
+
raw = fs.readFileSync(this.stateFilePath, 'utf8');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (errorCode(error) === 'ENOENT') {
|
|
48
|
+
return { type: 'absent' };
|
|
49
|
+
}
|
|
50
|
+
return this.unreadable(
|
|
51
|
+
`the stored watermark file could not be read (${String(error)})`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let parsed: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(raw);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return this.unreadable(
|
|
60
|
+
`the stored watermark file does not contain valid JSON (${String(error)})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isRecord(parsed)) {
|
|
65
|
+
return this.unreadable(
|
|
66
|
+
'the stored watermark file does not contain a JSON object',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lastReportedDateAdded = parsed.lastReportedDateAdded;
|
|
71
|
+
if (typeof lastReportedDateAdded !== 'string') {
|
|
72
|
+
return this.unreadable(
|
|
73
|
+
`the stored lastReportedDateAdded is not a string (${JSON.stringify(lastReportedDateAdded)})`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!isCalendarYmd(lastReportedDateAdded)) {
|
|
77
|
+
return this.unreadable(
|
|
78
|
+
`the stored lastReportedDateAdded is not a calendar date in YYYY-MM-DD form (${JSON.stringify(lastReportedDateAdded)})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const reportedCveIdsOnLastReportedDateAdded =
|
|
83
|
+
parsed.reportedCveIdsOnLastReportedDateAdded;
|
|
84
|
+
if (!Array.isArray(reportedCveIdsOnLastReportedDateAdded)) {
|
|
85
|
+
return this.unreadable(
|
|
86
|
+
'the stored reportedCveIdsOnLastReportedDateAdded is not an array',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
!reportedCveIdsOnLastReportedDateAdded.every(
|
|
91
|
+
(cveId) => typeof cveId === 'string',
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
return this.unreadable(
|
|
95
|
+
'the stored reportedCveIdsOnLastReportedDateAdded contains a non-string entry',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
type: 'stored',
|
|
101
|
+
watermark: {
|
|
102
|
+
lastReportedDateAdded,
|
|
103
|
+
reportedCveIdsOnLastReportedDateAdded:
|
|
104
|
+
reportedCveIdsOnLastReportedDateAdded,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
save = async (watermark: KevReportWatermark): Promise<void> => {
|
|
110
|
+
try {
|
|
111
|
+
const directory = path.dirname(this.stateFilePath);
|
|
112
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
113
|
+
const temporaryPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
114
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(watermark));
|
|
115
|
+
fs.renameSync(temporaryPath, this.stateFilePath);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(
|
|
118
|
+
`Unable to write the KEV report watermark to ${this.stateFilePath}: ${String(error)}`,
|
|
119
|
+
);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
private unreadable = (reason: string): KevReportWatermarkLoadResult => {
|
|
125
|
+
console.error(
|
|
126
|
+
`Unable to use the KEV report watermark stored at ${this.stateFilePath}: ${reason}. The file is left untouched for inspection.`,
|
|
127
|
+
);
|
|
128
|
+
return { type: 'unreadable', reason };
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -59,6 +59,7 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
59
59
|
silentSeconds: 180,
|
|
60
60
|
runningSeconds: 1200,
|
|
61
61
|
waitingOnExternalProcess: false,
|
|
62
|
+
finishedResultUnconsumed: false,
|
|
62
63
|
},
|
|
63
64
|
]);
|
|
64
65
|
});
|
|
@@ -88,6 +89,7 @@ describe('ProcessListSessionSubAgentActivityRepository', () => {
|
|
|
88
89
|
silentSeconds: 0,
|
|
89
90
|
runningSeconds: 30,
|
|
90
91
|
waitingOnExternalProcess: false,
|
|
92
|
+
finishedResultUnconsumed: false,
|
|
91
93
|
},
|
|
92
94
|
]);
|
|
93
95
|
});
|
|
@@ -44,6 +44,7 @@ export class ProcessListSessionSubAgentActivityRepository implements SessionSubA
|
|
|
44
44
|
silentSeconds: this.silentSecondsResolver.resolveSilentSeconds(label),
|
|
45
45
|
runningSeconds: process.elapsedSeconds,
|
|
46
46
|
waitingOnExternalProcess: false,
|
|
47
|
+
finishedResultUnconsumed: false,
|
|
47
48
|
};
|
|
48
49
|
const existing = result.get(sessionName);
|
|
49
50
|
if (existing === undefined) {
|
|
@@ -940,6 +940,9 @@ describe('TranscriptOwnerCallStatusProvider wired into NotifySilentLiveSessionsU
|
|
|
940
940
|
.fn()
|
|
941
941
|
.mockReturnValue('MAIN_STALLED_STALE'),
|
|
942
942
|
composeSubAgentSection: jest.fn().mockReturnValue('SUBAGENT'),
|
|
943
|
+
composeSubAgentUnconsumedResultSection: jest
|
|
944
|
+
.fn()
|
|
945
|
+
.mockReturnValue('SUBAGENT_UNCONSUMED'),
|
|
943
946
|
},
|
|
944
947
|
{ sleep: jest.fn().mockResolvedValue(undefined) },
|
|
945
948
|
);
|