github-issue-tower-defence-management 1.114.2 → 1.115.1
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 +3 -4
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +10 -17
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -4
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +1 -26
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +34 -9
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +6 -21
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +110 -62
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +8 -12
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +2 -98
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +1 -45
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +221 -11
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +55 -18
- package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +0 -5
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +3 -4
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +1 -6
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +7 -3
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +0 -2
- package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -1
|
@@ -3,9 +3,8 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { LocalCommandRunner } from '../../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
5
5
|
import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
6
|
-
import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
|
|
7
|
-
import { LocalStorageRepository } from '../../repositories/LocalStorageRepository';
|
|
8
6
|
import { SilentSessionMessageTemplates } from '../../repositories/ConfigurableSilentSessionMessageComposer';
|
|
7
|
+
import { Issue } from '../../../domain/entities/Issue';
|
|
9
8
|
import { SILENT_SESSION_REMINDER_SENTINEL } from '../../../domain/usecases/silentSessionReminderSentinel';
|
|
10
9
|
import {
|
|
11
10
|
notifySilentTmuxSessions,
|
|
@@ -37,18 +36,15 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
|
|
|
37
36
|
|
|
38
37
|
describe('notifySilentTmuxSessions', () => {
|
|
39
38
|
let configDir: string;
|
|
40
|
-
let cacheDirectory: string;
|
|
41
39
|
|
|
42
40
|
beforeEach(() => {
|
|
43
41
|
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
44
42
|
configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-config-'));
|
|
45
|
-
cacheDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-cache-'));
|
|
46
43
|
});
|
|
47
44
|
|
|
48
45
|
afterEach(() => {
|
|
49
46
|
jest.restoreAllMocks();
|
|
50
47
|
fs.rmSync(configDir, { force: true, recursive: true });
|
|
51
|
-
fs.rmSync(cacheDirectory, { force: true, recursive: true });
|
|
52
48
|
});
|
|
53
49
|
|
|
54
50
|
const writeTranscript = (lines: object[]): void => {
|
|
@@ -77,17 +73,6 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
77
73
|
]);
|
|
78
74
|
};
|
|
79
75
|
|
|
80
|
-
const makeCacheRepository = (): LocalStorageCacheRepository =>
|
|
81
|
-
new LocalStorageCacheRepository(
|
|
82
|
-
new LocalStorageRepository(),
|
|
83
|
-
cacheDirectory,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const makeCacheRepositoryAt = (
|
|
87
|
-
basePath: string,
|
|
88
|
-
): LocalStorageCacheRepository =>
|
|
89
|
-
new LocalStorageCacheRepository(new LocalStorageRepository(), basePath);
|
|
90
|
-
|
|
91
76
|
const makeEnvironReader = (): ProcessEnvironReader => ({
|
|
92
77
|
readEnviron: (pid: number) =>
|
|
93
78
|
pid === CLAUDE_PID
|
|
@@ -125,11 +110,12 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
125
110
|
enabled: true,
|
|
126
111
|
localCommandRunner: runner,
|
|
127
112
|
processEnvironReader: makeEnvironReader(),
|
|
128
|
-
cacheRepository: makeCacheRepository(),
|
|
129
113
|
ownerCallMarker: null,
|
|
130
114
|
subAgentOutputRootDirectory: null,
|
|
131
115
|
subAgentProcessMatchPattern: null,
|
|
132
116
|
subAgentTranscriptRootDirectory: null,
|
|
117
|
+
activeHubTaskStatus: null,
|
|
118
|
+
hubTaskStatusResolver: null,
|
|
133
119
|
messageTemplates: EMPTY_TEMPLATES,
|
|
134
120
|
now: NOW,
|
|
135
121
|
...DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
@@ -216,81 +202,143 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
216
202
|
expect(sendCall).toBeUndefined();
|
|
217
203
|
});
|
|
218
204
|
|
|
219
|
-
it('
|
|
205
|
+
it('re-notifies the same silent session on the next cycle with no cooldown suppression', async () => {
|
|
220
206
|
silentAssistantTranscript();
|
|
221
|
-
const cacheRepository = makeCacheRepository();
|
|
222
207
|
const firstRunner = liveSessionRunner();
|
|
223
208
|
|
|
224
|
-
await notifySilentTmuxSessions(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
209
|
+
await notifySilentTmuxSessions(baseParams(firstRunner));
|
|
210
|
+
|
|
211
|
+
const firstSendCall = firstRunner.runCommand.mock.calls.find(
|
|
212
|
+
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
213
|
+
);
|
|
214
|
+
expect(firstSendCall?.[1][2]).toBe(SESSION_NAME);
|
|
228
215
|
|
|
229
216
|
const secondRunner = liveSessionRunner();
|
|
230
217
|
await notifySilentTmuxSessions({
|
|
231
218
|
...baseParams(secondRunner),
|
|
232
|
-
cacheRepository,
|
|
233
219
|
now: new Date(NOW.getTime() + 60 * 1000),
|
|
234
220
|
});
|
|
235
221
|
|
|
236
222
|
const secondSendCall = secondRunner.runCommand.mock.calls.find(
|
|
237
223
|
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
238
224
|
);
|
|
239
|
-
expect(secondSendCall).
|
|
225
|
+
expect(secondSendCall?.[1][2]).toBe(SESSION_NAME);
|
|
240
226
|
});
|
|
241
227
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// The handler runs once per project over the same global set of tmux
|
|
245
|
-
// sessions. When the silent-session cooldown lives in one shared cache
|
|
246
|
-
// scope, a second per-project pass for the same session within the cooldown
|
|
247
|
-
// window must NOT re-send, even though it is a different project pass.
|
|
248
|
-
const sharedCacheBasePath = path.join(cacheDirectory, 'shared');
|
|
249
|
-
const firstRunner = liveSessionRunner();
|
|
228
|
+
const HUB_TASK_SESSION_NAME =
|
|
229
|
+
'https://github.com/HiromiShikata/repo/issues/42';
|
|
250
230
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
231
|
+
const writeUrlSessionTranscript = (): void => {
|
|
232
|
+
const projectDirectory = path.join(configDir, 'projects', '-home-user');
|
|
233
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
234
|
+
const silentTimestamp = new Date(
|
|
235
|
+
(NOW_EPOCH_SECONDS - 11 * 60) * 1000,
|
|
236
|
+
).toISOString();
|
|
237
|
+
fs.writeFileSync(
|
|
238
|
+
path.join(projectDirectory, `${SESSION_ID}.jsonl`),
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
type: 'assistant',
|
|
241
|
+
timestamp: silentTimestamp,
|
|
242
|
+
message: {
|
|
243
|
+
role: 'assistant',
|
|
244
|
+
content: [{ type: 'text', text: 'progress update' }],
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
'utf8',
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const urlSessionRunner = (): Mocked<LocalCommandRunner> => {
|
|
252
|
+
const runner = createMockRunner();
|
|
253
|
+
runner.runCommand.mockImplementation(async (program, args) => {
|
|
254
|
+
if (program === 'tmux' && args[0] === 'list-sessions') {
|
|
255
|
+
return {
|
|
256
|
+
stdout: `${HUB_TASK_SESSION_NAME}\n`,
|
|
257
|
+
stderr: '',
|
|
258
|
+
exitCode: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (program === 'tmux' && args[0] === 'list-panes') {
|
|
262
|
+
return { stdout: `${PANE_PID}\n`, stderr: '', exitCode: 0 };
|
|
263
|
+
}
|
|
264
|
+
if (program === 'ps') {
|
|
265
|
+
return {
|
|
266
|
+
stdout: ` ${PANE_PID} 1 shell\n ${CLAUDE_PID} ${PANE_PID} claude --name ${HUB_TASK_SESSION_NAME}\n`,
|
|
267
|
+
stderr: '',
|
|
268
|
+
exitCode: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
254
272
|
});
|
|
273
|
+
return runner;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const makeIssue = (overrides: {
|
|
277
|
+
state: Issue['state'];
|
|
278
|
+
status: string | null;
|
|
279
|
+
}): Issue => ({
|
|
280
|
+
nameWithOwner: 'HiromiShikata/repo',
|
|
281
|
+
number: 42,
|
|
282
|
+
title: 'Hub task',
|
|
283
|
+
state: overrides.state,
|
|
284
|
+
status: overrides.status,
|
|
285
|
+
story: null,
|
|
286
|
+
nextActionDate: null,
|
|
287
|
+
nextActionHour: null,
|
|
288
|
+
estimationMinutes: null,
|
|
289
|
+
dependedIssueUrls: [],
|
|
290
|
+
completionDate50PercentConfidence: null,
|
|
291
|
+
url: HUB_TASK_SESSION_NAME,
|
|
292
|
+
assignees: [],
|
|
293
|
+
labels: [],
|
|
294
|
+
org: 'HiromiShikata',
|
|
295
|
+
repo: 'repo',
|
|
296
|
+
body: '',
|
|
297
|
+
itemId: 'item-id',
|
|
298
|
+
isPr: false,
|
|
299
|
+
isInProgress: false,
|
|
300
|
+
isClosed: overrides.state !== 'OPEN',
|
|
301
|
+
createdAt: NOW,
|
|
302
|
+
author: 'HiromiShikata',
|
|
303
|
+
closingIssueReferenceUrls: [],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('skips a URL-named session whose hub task is no longer in the active status', async () => {
|
|
307
|
+
writeUrlSessionTranscript();
|
|
308
|
+
const runner = urlSessionRunner();
|
|
309
|
+
const getIssueByUrl = jest
|
|
310
|
+
.fn()
|
|
311
|
+
.mockResolvedValue(makeIssue({ state: 'OPEN', status: 'Todo' }));
|
|
255
312
|
|
|
256
|
-
const secondRunner = liveSessionRunner();
|
|
257
313
|
await notifySilentTmuxSessions({
|
|
258
|
-
...baseParams(
|
|
259
|
-
|
|
260
|
-
|
|
314
|
+
...baseParams(runner),
|
|
315
|
+
activeHubTaskStatus: 'In tmux',
|
|
316
|
+
hubTaskStatusResolver: { getIssueByUrl },
|
|
261
317
|
});
|
|
262
318
|
|
|
263
|
-
|
|
319
|
+
expect(getIssueByUrl).toHaveBeenCalledWith(HUB_TASK_SESSION_NAME);
|
|
320
|
+
const sendCall = runner.runCommand.mock.calls.find(
|
|
264
321
|
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
265
322
|
);
|
|
266
|
-
expect(
|
|
323
|
+
expect(sendCall).toBeUndefined();
|
|
267
324
|
});
|
|
268
325
|
|
|
269
|
-
it('
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
await notifySilentTmuxSessions({
|
|
276
|
-
...baseParams(firstRunner),
|
|
277
|
-
cacheRepository: makeCacheRepositoryAt(
|
|
278
|
-
path.join(cacheDirectory, 'umino'),
|
|
279
|
-
),
|
|
280
|
-
});
|
|
326
|
+
it('sends to a URL-named session whose hub task is in the active status', async () => {
|
|
327
|
+
writeUrlSessionTranscript();
|
|
328
|
+
const runner = urlSessionRunner();
|
|
329
|
+
const getIssueByUrl = jest
|
|
330
|
+
.fn()
|
|
331
|
+
.mockResolvedValue(makeIssue({ state: 'OPEN', status: 'In tmux' }));
|
|
281
332
|
|
|
282
|
-
const secondRunner = liveSessionRunner();
|
|
283
333
|
await notifySilentTmuxSessions({
|
|
284
|
-
...baseParams(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
),
|
|
288
|
-
now: new Date(NOW.getTime() + 60 * 1000),
|
|
334
|
+
...baseParams(runner),
|
|
335
|
+
activeHubTaskStatus: 'In tmux',
|
|
336
|
+
hubTaskStatusResolver: { getIssueByUrl },
|
|
289
337
|
});
|
|
290
338
|
|
|
291
|
-
const
|
|
339
|
+
const sendCall = runner.runCommand.mock.calls.find(
|
|
292
340
|
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
293
341
|
);
|
|
294
|
-
expect(
|
|
342
|
+
expect(sendCall?.[1][2]).toBe(HUB_TASK_SESSION_NAME);
|
|
295
343
|
});
|
|
296
344
|
});
|
|
@@ -4,10 +4,10 @@ import { OwnerCallStatusProvider } from '../../../domain/usecases/adapter-interf
|
|
|
4
4
|
import { ProcessEnvironReader } from '../../../domain/usecases/adapter-interfaces/ProcessEnvironReader';
|
|
5
5
|
import {
|
|
6
6
|
NotifySilentLiveSessionsUseCase,
|
|
7
|
+
HubTaskStatusResolver,
|
|
7
8
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
8
9
|
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
9
10
|
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
10
|
-
DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
|
|
11
11
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
12
12
|
} from '../../../domain/usecases/NotifySilentLiveSessionsUseCase';
|
|
13
13
|
import { DefaultSilentSessionMessageComposer } from '../../../domain/usecases/DefaultSilentSessionMessageComposer';
|
|
@@ -16,7 +16,6 @@ import { ProcFsProcessEnvironReader } from '../../repositories/ProcFsProcessEnvi
|
|
|
16
16
|
import { FileSystemInteractiveLiveSessionTranscriptResolver } from '../../repositories/FileSystemInteractiveLiveSessionTranscriptResolver';
|
|
17
17
|
import { FileSystemSessionOutputActivityRepository } from '../../repositories/FileSystemSessionOutputActivityRepository';
|
|
18
18
|
import { TmuxSilentSessionNotificationRepository } from '../../repositories/TmuxSilentSessionNotificationRepository';
|
|
19
|
-
import { LocalStorageCacheRepository } from '../../repositories/LocalStorageCacheRepository';
|
|
20
19
|
import { NoUnansweredOwnerCallStatusProvider } from '../../repositories/NoUnansweredOwnerCallStatusProvider';
|
|
21
20
|
import { TranscriptOwnerCallStatusProvider } from '../../repositories/TranscriptOwnerCallStatusProvider';
|
|
22
21
|
import { ProcessListSessionSubAgentActivityRepository } from '../../repositories/ProcessListSessionSubAgentActivityRepository';
|
|
@@ -34,7 +33,6 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
34
33
|
enabled: boolean;
|
|
35
34
|
localCommandRunner: LocalCommandRunner;
|
|
36
35
|
processEnvironReader?: ProcessEnvironReader;
|
|
37
|
-
cacheRepository: Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>;
|
|
38
36
|
ownerCallMarker: string | null;
|
|
39
37
|
subAgentOutputRootDirectory: string | null;
|
|
40
38
|
subAgentProcessMatchPattern: string | null;
|
|
@@ -42,8 +40,9 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
42
40
|
mainSilentThresholdSeconds: number;
|
|
43
41
|
subAgentSilentThresholdSeconds: number;
|
|
44
42
|
subAgentRunningThresholdSeconds: number;
|
|
45
|
-
cooldownSeconds: number;
|
|
46
43
|
staggerSeconds: number;
|
|
44
|
+
activeHubTaskStatus: string | null;
|
|
45
|
+
hubTaskStatusResolver: HubTaskStatusResolver | null;
|
|
47
46
|
messageTemplates: SilentSessionMessageTemplates;
|
|
48
47
|
now: Date;
|
|
49
48
|
};
|
|
@@ -89,7 +88,6 @@ export const notifySilentTmuxSessions = async (
|
|
|
89
88
|
enabled,
|
|
90
89
|
localCommandRunner,
|
|
91
90
|
processEnvironReader,
|
|
92
|
-
cacheRepository,
|
|
93
91
|
ownerCallMarker,
|
|
94
92
|
subAgentOutputRootDirectory,
|
|
95
93
|
subAgentProcessMatchPattern,
|
|
@@ -97,8 +95,9 @@ export const notifySilentTmuxSessions = async (
|
|
|
97
95
|
mainSilentThresholdSeconds,
|
|
98
96
|
subAgentSilentThresholdSeconds,
|
|
99
97
|
subAgentRunningThresholdSeconds,
|
|
100
|
-
cooldownSeconds,
|
|
101
98
|
staggerSeconds,
|
|
99
|
+
activeHubTaskStatus,
|
|
100
|
+
hubTaskStatusResolver,
|
|
102
101
|
messageTemplates,
|
|
103
102
|
now,
|
|
104
103
|
} = params;
|
|
@@ -127,19 +126,17 @@ export const notifySilentTmuxSessions = async (
|
|
|
127
126
|
now,
|
|
128
127
|
),
|
|
129
128
|
createOwnerCallStatusProvider(ownerCallMarker),
|
|
130
|
-
new TmuxSilentSessionNotificationRepository(
|
|
131
|
-
localCommandRunner,
|
|
132
|
-
cacheRepository,
|
|
133
|
-
),
|
|
129
|
+
new TmuxSilentSessionNotificationRepository(localCommandRunner),
|
|
134
130
|
messageComposer,
|
|
135
131
|
new RealSleeper(),
|
|
132
|
+
hubTaskStatusResolver,
|
|
136
133
|
);
|
|
137
134
|
await useCase.run({
|
|
138
135
|
mainSilentThresholdSeconds,
|
|
139
136
|
subAgentSilentThresholdSeconds,
|
|
140
137
|
subAgentRunningThresholdSeconds,
|
|
141
|
-
cooldownSeconds,
|
|
142
138
|
staggerSeconds,
|
|
139
|
+
activeHubTaskStatus,
|
|
143
140
|
now,
|
|
144
141
|
});
|
|
145
142
|
};
|
|
@@ -148,6 +145,5 @@ export const DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS = {
|
|
|
148
145
|
mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
149
146
|
subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
150
147
|
subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
151
|
-
cooldownSeconds: DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
|
|
152
148
|
staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
153
149
|
} as const;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
2
|
-
import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
|
|
3
2
|
import { TmuxSilentSessionNotificationRepository } from './TmuxSilentSessionNotificationRepository';
|
|
4
3
|
|
|
5
4
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
@@ -12,22 +11,11 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
|
|
|
12
11
|
}),
|
|
13
12
|
});
|
|
14
13
|
|
|
15
|
-
const createMockCacheRepository = (): Mocked<
|
|
16
|
-
Pick<LocalStorageCacheRepository, 'getLatest' | 'set'>
|
|
17
|
-
> => ({
|
|
18
|
-
getLatest: jest.fn().mockResolvedValue(null),
|
|
19
|
-
set: jest.fn().mockResolvedValue(undefined),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
14
|
describe('TmuxSilentSessionNotificationRepository', () => {
|
|
23
15
|
describe('sendSelfCheckNotification', () => {
|
|
24
16
|
it('sends the message literally then submits it with Enter', async () => {
|
|
25
17
|
const runner = createMockRunner();
|
|
26
|
-
const
|
|
27
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
28
|
-
runner,
|
|
29
|
-
cache,
|
|
30
|
-
);
|
|
18
|
+
const repository = new TmuxSilentSessionNotificationRepository(runner);
|
|
31
19
|
|
|
32
20
|
await repository.sendSelfCheckNotification(
|
|
33
21
|
'https_//github_com/owner/repo/issues/9',
|
|
@@ -57,11 +45,7 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
57
45
|
stderr: "can't find session",
|
|
58
46
|
exitCode: 1,
|
|
59
47
|
});
|
|
60
|
-
const
|
|
61
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
62
|
-
runner,
|
|
63
|
-
cache,
|
|
64
|
-
);
|
|
48
|
+
const repository = new TmuxSilentSessionNotificationRepository(runner);
|
|
65
49
|
|
|
66
50
|
await expect(
|
|
67
51
|
repository.sendSelfCheckNotification('missing_session', 'message'),
|
|
@@ -70,84 +54,4 @@ describe('TmuxSilentSessionNotificationRepository', () => {
|
|
|
70
54
|
);
|
|
71
55
|
});
|
|
72
56
|
});
|
|
73
|
-
|
|
74
|
-
describe('getLastNotifiedEpochSeconds', () => {
|
|
75
|
-
it('returns the cached epoch seconds for a session', async () => {
|
|
76
|
-
const runner = createMockRunner();
|
|
77
|
-
const cache = createMockCacheRepository();
|
|
78
|
-
cache.getLatest.mockResolvedValue({
|
|
79
|
-
value: { epochSeconds: 1700000000 },
|
|
80
|
-
timestamp: new Date(),
|
|
81
|
-
});
|
|
82
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
83
|
-
runner,
|
|
84
|
-
cache,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const result = await repository.getLastNotifiedEpochSeconds(
|
|
88
|
-
'https_//github_com/owner/repo/issues/9',
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
expect(result).toBe(1700000000);
|
|
92
|
-
expect(cache.getLatest).toHaveBeenCalledWith(
|
|
93
|
-
'silent-session-notification/https___github_com_owner_repo_issues_9',
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('returns null when no cache entry exists', async () => {
|
|
98
|
-
const runner = createMockRunner();
|
|
99
|
-
const cache = createMockCacheRepository();
|
|
100
|
-
cache.getLatest.mockResolvedValue(null);
|
|
101
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
102
|
-
runner,
|
|
103
|
-
cache,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const result = await repository.getLastNotifiedEpochSeconds(
|
|
107
|
-
'https_//github_com/owner/repo/issues/9',
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
expect(result).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('returns null when the cached value has no numeric epoch', async () => {
|
|
114
|
-
const runner = createMockRunner();
|
|
115
|
-
const cache = createMockCacheRepository();
|
|
116
|
-
cache.getLatest.mockResolvedValue({
|
|
117
|
-
value: {},
|
|
118
|
-
timestamp: new Date(),
|
|
119
|
-
});
|
|
120
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
121
|
-
runner,
|
|
122
|
-
cache,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const result = await repository.getLastNotifiedEpochSeconds(
|
|
126
|
-
'https_//github_com/owner/repo/issues/9',
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
expect(result).toBeNull();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('setLastNotifiedEpochSeconds', () => {
|
|
134
|
-
it('persists the epoch seconds keyed by the sanitized session name', async () => {
|
|
135
|
-
const runner = createMockRunner();
|
|
136
|
-
const cache = createMockCacheRepository();
|
|
137
|
-
const repository = new TmuxSilentSessionNotificationRepository(
|
|
138
|
-
runner,
|
|
139
|
-
cache,
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
await repository.setLastNotifiedEpochSeconds(
|
|
143
|
-
'https_//github_com/owner/repo/issues/9',
|
|
144
|
-
1700000000,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
expect(cache.set).toHaveBeenCalledWith(
|
|
148
|
-
'silent-session-notification/https___github_com_owner_repo_issues_9',
|
|
149
|
-
{ epochSeconds: 1700000000 },
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
57
|
});
|
|
@@ -1,49 +1,8 @@
|
|
|
1
1
|
import { SilentSessionNotificationRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionNotificationRepository';
|
|
2
2
|
import { LocalCommandRunner } from '../../domain/usecases/adapter-interfaces/LocalCommandRunner';
|
|
3
|
-
import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
|
|
4
|
-
|
|
5
|
-
const CACHE_KEY_PREFIX = 'silent-session-notification';
|
|
6
|
-
|
|
7
|
-
const readEpochSeconds = (value: object): number | null => {
|
|
8
|
-
if (!('epochSeconds' in value)) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
const candidate = value.epochSeconds;
|
|
12
|
-
if (typeof candidate !== 'number') {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
return candidate;
|
|
16
|
-
};
|
|
17
3
|
|
|
18
4
|
export class TmuxSilentSessionNotificationRepository implements SilentSessionNotificationRepository {
|
|
19
|
-
constructor(
|
|
20
|
-
private readonly localCommandRunner: LocalCommandRunner,
|
|
21
|
-
private readonly cacheRepository: Pick<
|
|
22
|
-
LocalStorageCacheRepository,
|
|
23
|
-
'getLatest' | 'set'
|
|
24
|
-
>,
|
|
25
|
-
) {}
|
|
26
|
-
|
|
27
|
-
getLastNotifiedEpochSeconds = async (
|
|
28
|
-
sessionName: string,
|
|
29
|
-
): Promise<number | null> => {
|
|
30
|
-
const cached = await this.cacheRepository.getLatest(
|
|
31
|
-
this.toCacheKey(sessionName),
|
|
32
|
-
);
|
|
33
|
-
if (cached === null) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return readEpochSeconds(cached.value);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
setLastNotifiedEpochSeconds = async (
|
|
40
|
-
sessionName: string,
|
|
41
|
-
epochSeconds: number,
|
|
42
|
-
): Promise<void> => {
|
|
43
|
-
await this.cacheRepository.set(this.toCacheKey(sessionName), {
|
|
44
|
-
epochSeconds,
|
|
45
|
-
});
|
|
46
|
-
};
|
|
5
|
+
constructor(private readonly localCommandRunner: LocalCommandRunner) {}
|
|
47
6
|
|
|
48
7
|
sendSelfCheckNotification = async (
|
|
49
8
|
sessionName: string,
|
|
@@ -77,7 +36,4 @@ export class TmuxSilentSessionNotificationRepository implements SilentSessionNot
|
|
|
77
36
|
);
|
|
78
37
|
}
|
|
79
38
|
};
|
|
80
|
-
|
|
81
|
-
private toCacheKey = (sessionName: string): string =>
|
|
82
|
-
`${CACHE_KEY_PREFIX}/${sessionName.replace(/\//g, '_')}`;
|
|
83
39
|
}
|