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.
Files changed (28) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -4
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +10 -17
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +3 -4
  6. package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
  7. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +1 -26
  8. package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -1
  9. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +34 -9
  10. package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +6 -21
  13. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +110 -62
  14. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +8 -12
  15. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +2 -98
  16. package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +1 -45
  17. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +221 -11
  18. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +55 -18
  19. package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +0 -5
  20. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  21. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +3 -4
  22. package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
  23. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +1 -6
  24. package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -1
  25. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +7 -3
  26. package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
  27. package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +0 -2
  28. 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('does not re-notify the same silent session on the next cycle within cooldown', async () => {
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
- ...baseParams(firstRunner),
226
- cacheRepository,
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).toBeUndefined();
225
+ expect(secondSendCall?.[1][2]).toBe(SESSION_NAME);
240
226
  });
241
227
 
242
- it('shares the cooldown across project passes when a single project-independent cache scope is used', async () => {
243
- silentAssistantTranscript();
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
- await notifySilentTmuxSessions({
252
- ...baseParams(firstRunner),
253
- cacheRepository: makeCacheRepositoryAt(sharedCacheBasePath),
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(secondRunner),
259
- cacheRepository: makeCacheRepositoryAt(sharedCacheBasePath),
260
- now: new Date(NOW.getTime() + 60 * 1000),
314
+ ...baseParams(runner),
315
+ activeHubTaskStatus: 'In tmux',
316
+ hubTaskStatusResolver: { getIssueByUrl },
261
317
  });
262
318
 
263
- const secondSendCall = secondRunner.runCommand.mock.calls.find(
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(secondSendCall).toBeUndefined();
323
+ expect(sendCall).toBeUndefined();
267
324
  });
268
325
 
269
- it('re-sends within the cooldown window when each project pass uses its own cache scope', async () => {
270
- silentAssistantTranscript();
271
- // This reproduces the per-project cooldown defect: when each project pass
272
- // stores its cooldown under its own project-scoped cache path, the same
273
- // session is notified once per project within the cooldown window.
274
- const firstRunner = liveSessionRunner();
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(secondRunner),
285
- cacheRepository: makeCacheRepositoryAt(
286
- path.join(cacheDirectory, 'xmile'),
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 secondSendCall = secondRunner.runCommand.mock.calls.find(
339
+ const sendCall = runner.runCommand.mock.calls.find(
292
340
  (call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
293
341
  );
294
- expect(secondSendCall?.[1][2]).toBe(SESSION_NAME);
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 cache = createMockCacheRepository();
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 cache = createMockCacheRepository();
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
  }