github-issue-tower-defence-management 1.117.7 → 1.117.9
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 +5 -3
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +14 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +7 -2
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +128 -0
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +3 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +17 -5
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +37 -3
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +14 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +201 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +115 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +30 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +4 -1
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +149 -26
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +24 -4
- package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +10 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +3 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +18 -0
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +11 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotifySilentLiveSessionsUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifySilentLiveSessionsUseCase.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"NotifySilentLiveSessionsUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/NotifySilentLiveSessionsUseCase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SilentSessionCandidateStateRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -117,6 +117,8 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
117
117
|
subAgentSilentThresholdSeconds?: number;
|
|
118
118
|
subAgentRunningThresholdSeconds?: number;
|
|
119
119
|
silentNotificationStaggerSeconds?: number;
|
|
120
|
+
candidateDebounceRecencyWindowSeconds?: number;
|
|
121
|
+
candidateDebounceStateFilePath?: string;
|
|
120
122
|
activeHubTaskStatus?: string;
|
|
121
123
|
silentMainStalledMessage?: string;
|
|
122
124
|
silentSubAgentMessageHeader?: string;
|
|
@@ -608,6 +610,15 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
608
610
|
process.env.TDPM_SILENT_NOTIFICATION_STAGGER_SECONDS,
|
|
609
611
|
DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.staggerSeconds,
|
|
610
612
|
),
|
|
613
|
+
candidateDebounceRecencyWindowSeconds: readSilentSeconds(
|
|
614
|
+
mergedInput.candidateDebounceRecencyWindowSeconds,
|
|
615
|
+
process.env.TDPM_SILENT_CANDIDATE_DEBOUNCE_RECENCY_WINDOW_SECONDS,
|
|
616
|
+
DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS.candidateDebounceRecencyWindowSeconds,
|
|
617
|
+
),
|
|
618
|
+
candidateDebounceStateFilePath:
|
|
619
|
+
mergedInput.candidateDebounceStateFilePath ??
|
|
620
|
+
process.env.TDPM_SILENT_CANDIDATE_DEBOUNCE_STATE_FILE_PATH ??
|
|
621
|
+
null,
|
|
611
622
|
activeHubTaskStatus:
|
|
612
623
|
mergedInput.activeHubTaskStatus ??
|
|
613
624
|
process.env.TDPM_ACTIVE_HUB_TASK_STATUS ??
|
|
@@ -36,10 +36,15 @@ const createMockRunner = (): Mocked<LocalCommandRunner> => ({
|
|
|
36
36
|
|
|
37
37
|
describe('notifySilentTmuxSessions', () => {
|
|
38
38
|
let configDir: string;
|
|
39
|
+
let candidateStateFilePath: string;
|
|
39
40
|
|
|
40
41
|
beforeEach(() => {
|
|
41
42
|
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
42
43
|
configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'silent-config-'));
|
|
44
|
+
candidateStateFilePath = path.join(
|
|
45
|
+
configDir,
|
|
46
|
+
'silent-session-candidates.json',
|
|
47
|
+
);
|
|
43
48
|
});
|
|
44
49
|
|
|
45
50
|
afterEach(() => {
|
|
@@ -47,6 +52,18 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
47
52
|
fs.rmSync(configDir, { force: true, recursive: true });
|
|
48
53
|
});
|
|
49
54
|
|
|
55
|
+
const seedPreviousCandidates = (sessionNames: string[]): void => {
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
candidateStateFilePath,
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
candidates: sessionNames.map((sessionName) => ({
|
|
60
|
+
sessionName,
|
|
61
|
+
recordedEpochSeconds: NOW_EPOCH_SECONDS,
|
|
62
|
+
})),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
50
67
|
const writeTranscript = (lines: object[]): void => {
|
|
51
68
|
const projectDirectory = path.join(configDir, 'projects', '-home-user');
|
|
52
69
|
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
@@ -114,6 +131,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
114
131
|
subAgentOutputRootDirectory: null,
|
|
115
132
|
subAgentProcessMatchPattern: null,
|
|
116
133
|
subAgentTranscriptRootDirectory: null,
|
|
134
|
+
candidateDebounceStateFilePath: candidateStateFilePath,
|
|
117
135
|
activeHubTaskStatus: null,
|
|
118
136
|
hubTaskStatusResolver: null,
|
|
119
137
|
messageTemplates: EMPTY_TEMPLATES,
|
|
@@ -121,8 +139,9 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
121
139
|
...DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS,
|
|
122
140
|
});
|
|
123
141
|
|
|
124
|
-
it('sends a main stalled notification to a silent github-named live session', async () => {
|
|
142
|
+
it('sends a main stalled notification to a silent github-named live session that was already a candidate in the previous cycle', async () => {
|
|
125
143
|
silentAssistantTranscript();
|
|
144
|
+
seedPreviousCandidates([SESSION_NAME]);
|
|
126
145
|
const runner = liveSessionRunner();
|
|
127
146
|
|
|
128
147
|
await notifySilentTmuxSessions(baseParams(runner));
|
|
@@ -134,6 +153,18 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
134
153
|
expect(sendCall?.[1][4]).toContain('You have produced no output for');
|
|
135
154
|
});
|
|
136
155
|
|
|
156
|
+
it('does not notify a silent github-named live session on its first candidate cycle', async () => {
|
|
157
|
+
silentAssistantTranscript();
|
|
158
|
+
const runner = liveSessionRunner();
|
|
159
|
+
|
|
160
|
+
await notifySilentTmuxSessions(baseParams(runner));
|
|
161
|
+
|
|
162
|
+
const sendCall = runner.runCommand.mock.calls.find(
|
|
163
|
+
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
164
|
+
);
|
|
165
|
+
expect(sendCall).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
137
168
|
it('sends no notification to a silent non-github-named live session', async () => {
|
|
138
169
|
silentAssistantTranscript();
|
|
139
170
|
const runner = createMockRunner();
|
|
@@ -176,6 +207,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
176
207
|
|
|
177
208
|
it('uses the configured main stalled message template when provided', async () => {
|
|
178
209
|
silentAssistantTranscript();
|
|
210
|
+
seedPreviousCandidates([SESSION_NAME]);
|
|
179
211
|
const runner = liveSessionRunner();
|
|
180
212
|
|
|
181
213
|
await notifySilentTmuxSessions({
|
|
@@ -230,7 +262,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
230
262
|
expect(sendCall).toBeUndefined();
|
|
231
263
|
});
|
|
232
264
|
|
|
233
|
-
it('
|
|
265
|
+
it('defers the first cycle then notifies on the next cycle once the persisted candidate state confirms a second consecutive cycle', async () => {
|
|
234
266
|
silentAssistantTranscript();
|
|
235
267
|
const firstRunner = liveSessionRunner();
|
|
236
268
|
|
|
@@ -239,7 +271,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
239
271
|
const firstSendCall = firstRunner.runCommand.mock.calls.find(
|
|
240
272
|
(call) => call[0] === 'tmux' && call[1][0] === 'send-keys',
|
|
241
273
|
);
|
|
242
|
-
expect(firstSendCall
|
|
274
|
+
expect(firstSendCall).toBeUndefined();
|
|
243
275
|
|
|
244
276
|
const secondRunner = liveSessionRunner();
|
|
245
277
|
await notifySilentTmuxSessions({
|
|
@@ -333,6 +365,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
333
365
|
|
|
334
366
|
it('skips a URL-named session whose hub task is no longer in the active status', async () => {
|
|
335
367
|
writeUrlSessionTranscript();
|
|
368
|
+
seedPreviousCandidates([HUB_TASK_SESSION_NAME]);
|
|
336
369
|
const runner = urlSessionRunner();
|
|
337
370
|
const getIssueByUrl = jest
|
|
338
371
|
.fn()
|
|
@@ -353,6 +386,7 @@ describe('notifySilentTmuxSessions', () => {
|
|
|
353
386
|
|
|
354
387
|
it('sends to a URL-named session whose hub task is in the active status', async () => {
|
|
355
388
|
writeUrlSessionTranscript();
|
|
389
|
+
seedPreviousCandidates([HUB_TASK_SESSION_NAME]);
|
|
356
390
|
const runner = urlSessionRunner();
|
|
357
391
|
const getIssueByUrl = jest
|
|
358
392
|
.fn()
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
10
10
|
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
11
11
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
12
|
+
DEFAULT_CANDIDATE_DEBOUNCE_RECENCY_WINDOW_SECONDS,
|
|
12
13
|
} from '../../../domain/usecases/NotifySilentLiveSessionsUseCase';
|
|
13
14
|
import { DefaultSilentSessionMessageComposer } from '../../../domain/usecases/DefaultSilentSessionMessageComposer';
|
|
14
15
|
import { LocalProcessLiveSessionProcessSnapshotProvider } from '../../repositories/LocalProcessLiveSessionProcessSnapshotProvider';
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
SilentSessionMessageTemplates,
|
|
29
30
|
} from '../../repositories/ConfigurableSilentSessionMessageComposer';
|
|
30
31
|
import { RealSleeper } from '../../repositories/RealSleeper';
|
|
32
|
+
import { FileSystemSilentSessionCandidateStateRepository } from '../../repositories/FileSystemSilentSessionCandidateStateRepository';
|
|
31
33
|
|
|
32
34
|
export type NotifySilentTmuxSessionsParams = {
|
|
33
35
|
enabled: boolean;
|
|
@@ -41,6 +43,8 @@ export type NotifySilentTmuxSessionsParams = {
|
|
|
41
43
|
subAgentSilentThresholdSeconds: number;
|
|
42
44
|
subAgentRunningThresholdSeconds: number;
|
|
43
45
|
staggerSeconds: number;
|
|
46
|
+
candidateDebounceRecencyWindowSeconds: number;
|
|
47
|
+
candidateDebounceStateFilePath: string | null;
|
|
44
48
|
activeHubTaskStatus: string | null;
|
|
45
49
|
hubTaskStatusResolver: HubTaskStatusResolver | null;
|
|
46
50
|
messageTemplates: SilentSessionMessageTemplates;
|
|
@@ -96,6 +100,8 @@ export const notifySilentTmuxSessions = async (
|
|
|
96
100
|
subAgentSilentThresholdSeconds,
|
|
97
101
|
subAgentRunningThresholdSeconds,
|
|
98
102
|
staggerSeconds,
|
|
103
|
+
candidateDebounceRecencyWindowSeconds,
|
|
104
|
+
candidateDebounceStateFilePath,
|
|
99
105
|
activeHubTaskStatus,
|
|
100
106
|
hubTaskStatusResolver,
|
|
101
107
|
messageTemplates,
|
|
@@ -127,6 +133,11 @@ export const notifySilentTmuxSessions = async (
|
|
|
127
133
|
),
|
|
128
134
|
createOwnerCallStatusProvider(ownerCallMarker),
|
|
129
135
|
new TmuxSilentSessionNotificationRepository(localCommandRunner),
|
|
136
|
+
candidateDebounceStateFilePath !== null
|
|
137
|
+
? new FileSystemSilentSessionCandidateStateRepository(
|
|
138
|
+
candidateDebounceStateFilePath,
|
|
139
|
+
)
|
|
140
|
+
: new FileSystemSilentSessionCandidateStateRepository(),
|
|
130
141
|
messageComposer,
|
|
131
142
|
new RealSleeper(),
|
|
132
143
|
hubTaskStatusResolver,
|
|
@@ -136,6 +147,7 @@ export const notifySilentTmuxSessions = async (
|
|
|
136
147
|
subAgentSilentThresholdSeconds,
|
|
137
148
|
subAgentRunningThresholdSeconds,
|
|
138
149
|
staggerSeconds,
|
|
150
|
+
candidateDebounceRecencyWindowSeconds,
|
|
139
151
|
activeHubTaskStatus,
|
|
140
152
|
now,
|
|
141
153
|
});
|
|
@@ -146,4 +158,6 @@ export const DEFAULT_NOTIFY_SILENT_TMUX_SESSIONS_PARAMS = {
|
|
|
146
158
|
subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
147
159
|
subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
148
160
|
staggerSeconds: DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
161
|
+
candidateDebounceRecencyWindowSeconds:
|
|
162
|
+
DEFAULT_CANDIDATE_DEBOUNCE_RECENCY_WINDOW_SECONDS,
|
|
149
163
|
} as const;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_STATE_RETENTION_WINDOW_SECONDS,
|
|
6
|
+
FileSystemSilentSessionCandidateStateRepository,
|
|
7
|
+
} from './FileSystemSilentSessionCandidateStateRepository';
|
|
8
|
+
|
|
9
|
+
describe('FileSystemSilentSessionCandidateStateRepository', () => {
|
|
10
|
+
let rootDirectory: string;
|
|
11
|
+
let stateFilePath: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
rootDirectory = fs.mkdtempSync(
|
|
15
|
+
path.join(os.tmpdir(), 'silent-session-candidate-state-'),
|
|
16
|
+
);
|
|
17
|
+
stateFilePath = path.join(rootDirectory, 'silent-session-candidates.json');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(rootDirectory, { force: true, recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const sessionAlpha = 'https://github.com/HiromiShikata/repo/issues/100';
|
|
25
|
+
const sessionBravo = 'https://github.com/HiromiShikata/repo/issues/200';
|
|
26
|
+
|
|
27
|
+
it('round-trips a saved candidate set so the next cycle reads it back', async () => {
|
|
28
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
29
|
+
stateFilePath,
|
|
30
|
+
);
|
|
31
|
+
const now = new Date('2026-06-26T00:00:00Z');
|
|
32
|
+
|
|
33
|
+
await repository.saveCandidateSessionNames({
|
|
34
|
+
sessionNames: [sessionAlpha, sessionBravo],
|
|
35
|
+
now,
|
|
36
|
+
});
|
|
37
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
38
|
+
now: new Date('2026-06-26T00:01:00Z'),
|
|
39
|
+
recencyWindowSeconds: 15 * 60,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(loaded).toEqual(new Set([sessionAlpha, sessionBravo]));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns an empty set when no state file exists yet', async () => {
|
|
46
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
47
|
+
stateFilePath,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
51
|
+
now: new Date('2026-06-26T00:00:00Z'),
|
|
52
|
+
recencyWindowSeconds: 15 * 60,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(loaded).toEqual(new Set<string>());
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('excludes an entry older than the recency window when loading', async () => {
|
|
59
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
60
|
+
stateFilePath,
|
|
61
|
+
);
|
|
62
|
+
const savedAt = new Date('2026-06-26T00:00:00Z');
|
|
63
|
+
|
|
64
|
+
await repository.saveCandidateSessionNames({
|
|
65
|
+
sessionNames: [sessionAlpha],
|
|
66
|
+
now: savedAt,
|
|
67
|
+
});
|
|
68
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
69
|
+
now: new Date('2026-06-26T00:16:00Z'),
|
|
70
|
+
recencyWindowSeconds: 15 * 60,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(loaded).toEqual(new Set<string>());
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('includes an entry recorded exactly at the recency-window boundary', async () => {
|
|
77
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
78
|
+
stateFilePath,
|
|
79
|
+
);
|
|
80
|
+
const savedAt = new Date('2026-06-26T00:00:00Z');
|
|
81
|
+
|
|
82
|
+
await repository.saveCandidateSessionNames({
|
|
83
|
+
sessionNames: [sessionAlpha],
|
|
84
|
+
now: savedAt,
|
|
85
|
+
});
|
|
86
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
87
|
+
now: new Date('2026-06-26T00:15:00Z'),
|
|
88
|
+
recencyWindowSeconds: 15 * 60,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(loaded).toEqual(new Set([sessionAlpha]));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('preserves another recent session recorded by a concurrent run when saving a disjoint set', async () => {
|
|
95
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
96
|
+
stateFilePath,
|
|
97
|
+
);
|
|
98
|
+
const firstSaveAt = new Date('2026-06-26T00:00:00Z');
|
|
99
|
+
const secondSaveAt = new Date('2026-06-26T00:00:30Z');
|
|
100
|
+
|
|
101
|
+
await repository.saveCandidateSessionNames({
|
|
102
|
+
sessionNames: [sessionAlpha],
|
|
103
|
+
now: firstSaveAt,
|
|
104
|
+
});
|
|
105
|
+
await repository.saveCandidateSessionNames({
|
|
106
|
+
sessionNames: [sessionBravo],
|
|
107
|
+
now: secondSaveAt,
|
|
108
|
+
});
|
|
109
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
110
|
+
now: new Date('2026-06-26T00:01:00Z'),
|
|
111
|
+
recencyWindowSeconds: 15 * 60,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(loaded).toEqual(new Set([sessionAlpha, sessionBravo]));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('drops a stored entry that has aged beyond the retention window on the next save', async () => {
|
|
118
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
119
|
+
stateFilePath,
|
|
120
|
+
60 * 60,
|
|
121
|
+
);
|
|
122
|
+
const firstSaveAt = new Date('2026-06-26T00:00:00Z');
|
|
123
|
+
const secondSaveAt = new Date('2026-06-26T01:30:00Z');
|
|
124
|
+
|
|
125
|
+
await repository.saveCandidateSessionNames({
|
|
126
|
+
sessionNames: [sessionAlpha],
|
|
127
|
+
now: firstSaveAt,
|
|
128
|
+
});
|
|
129
|
+
await repository.saveCandidateSessionNames({
|
|
130
|
+
sessionNames: [sessionBravo],
|
|
131
|
+
now: secondSaveAt,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const persisted: unknown = JSON.parse(
|
|
135
|
+
fs.readFileSync(stateFilePath, 'utf8'),
|
|
136
|
+
);
|
|
137
|
+
expect(persisted).toEqual({
|
|
138
|
+
candidates: [
|
|
139
|
+
{
|
|
140
|
+
sessionName: sessionBravo,
|
|
141
|
+
recordedEpochSeconds: Math.floor(secondSaveAt.getTime() / 1000),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('refreshes the timestamp of a session that is a candidate again', async () => {
|
|
148
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
149
|
+
stateFilePath,
|
|
150
|
+
);
|
|
151
|
+
const firstSaveAt = new Date('2026-06-26T00:00:00Z');
|
|
152
|
+
const secondSaveAt = new Date('2026-06-26T00:14:00Z');
|
|
153
|
+
|
|
154
|
+
await repository.saveCandidateSessionNames({
|
|
155
|
+
sessionNames: [sessionAlpha],
|
|
156
|
+
now: firstSaveAt,
|
|
157
|
+
});
|
|
158
|
+
await repository.saveCandidateSessionNames({
|
|
159
|
+
sessionNames: [sessionAlpha],
|
|
160
|
+
now: secondSaveAt,
|
|
161
|
+
});
|
|
162
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
163
|
+
now: new Date('2026-06-26T00:20:00Z'),
|
|
164
|
+
recencyWindowSeconds: 15 * 60,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(loaded).toEqual(new Set([sessionAlpha]));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('treats a corrupt state file as no previous candidates', async () => {
|
|
171
|
+
fs.writeFileSync(stateFilePath, 'not valid json');
|
|
172
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
173
|
+
stateFilePath,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const loaded = await repository.loadRecentCandidateSessionNames({
|
|
177
|
+
now: new Date('2026-06-26T00:00:00Z'),
|
|
178
|
+
recencyWindowSeconds: 15 * 60,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(loaded).toEqual(new Set<string>());
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('creates the parent directory when the configured path does not exist yet', async () => {
|
|
185
|
+
const nestedPath = path.join(rootDirectory, 'nested', 'dir', 'state.json');
|
|
186
|
+
const repository = new FileSystemSilentSessionCandidateStateRepository(
|
|
187
|
+
nestedPath,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await repository.saveCandidateSessionNames({
|
|
191
|
+
sessionNames: [sessionAlpha],
|
|
192
|
+
now: new Date('2026-06-26T00:00:00Z'),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(fs.existsSync(nestedPath)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('exposes the default retention window as a named constant', () => {
|
|
199
|
+
expect(DEFAULT_STATE_RETENTION_WINDOW_SECONDS).toBe(60 * 60);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { SilentSessionCandidateStateRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository';
|
|
5
|
+
|
|
6
|
+
type StoredCandidateEntry = {
|
|
7
|
+
sessionName: string;
|
|
8
|
+
recordedEpochSeconds: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_STATE_RETENTION_WINDOW_SECONDS = 60 * 60;
|
|
15
|
+
|
|
16
|
+
const defaultStateFilePath = (): string => {
|
|
17
|
+
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
18
|
+
return path.join(base, 'tdpm', 'silent-session-candidates.json');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class FileSystemSilentSessionCandidateStateRepository implements SilentSessionCandidateStateRepository {
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly stateFilePath: string = defaultStateFilePath(),
|
|
24
|
+
private readonly retentionWindowSeconds: number = DEFAULT_STATE_RETENTION_WINDOW_SECONDS,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
loadRecentCandidateSessionNames = async (params: {
|
|
28
|
+
now: Date;
|
|
29
|
+
recencyWindowSeconds: number;
|
|
30
|
+
}): Promise<Set<string>> => {
|
|
31
|
+
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
32
|
+
const oldestAllowedEpochSeconds =
|
|
33
|
+
nowEpochSeconds - params.recencyWindowSeconds;
|
|
34
|
+
const entries = this.readEntries();
|
|
35
|
+
const recentSessionNames = new Set<string>();
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry.recordedEpochSeconds >= oldestAllowedEpochSeconds) {
|
|
38
|
+
recentSessionNames.add(entry.sessionName);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return recentSessionNames;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
saveCandidateSessionNames = async (params: {
|
|
45
|
+
sessionNames: string[];
|
|
46
|
+
now: Date;
|
|
47
|
+
}): Promise<void> => {
|
|
48
|
+
const recordedEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
49
|
+
const oldestRetainedEpochSeconds =
|
|
50
|
+
recordedEpochSeconds - this.retentionWindowSeconds;
|
|
51
|
+
const currentSessionNames = new Set(params.sessionNames);
|
|
52
|
+
const mergedBySessionName = new Map<string, StoredCandidateEntry>();
|
|
53
|
+
for (const entry of this.readEntries()) {
|
|
54
|
+
if (
|
|
55
|
+
entry.recordedEpochSeconds >= oldestRetainedEpochSeconds &&
|
|
56
|
+
!currentSessionNames.has(entry.sessionName)
|
|
57
|
+
) {
|
|
58
|
+
mergedBySessionName.set(entry.sessionName, entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const sessionName of currentSessionNames) {
|
|
62
|
+
mergedBySessionName.set(sessionName, {
|
|
63
|
+
sessionName,
|
|
64
|
+
recordedEpochSeconds,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
this.writeEntries(Array.from(mergedBySessionName.values()));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private readEntries = (): StoredCandidateEntry[] => {
|
|
71
|
+
let raw: string;
|
|
72
|
+
try {
|
|
73
|
+
raw = fs.readFileSync(this.stateFilePath, 'utf8');
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
let parsed: unknown;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (!isRecord(parsed)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const storedEntries = parsed.candidates;
|
|
87
|
+
if (!Array.isArray(storedEntries)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const entries: StoredCandidateEntry[] = [];
|
|
91
|
+
for (const storedEntry of storedEntries) {
|
|
92
|
+
if (!isRecord(storedEntry)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const sessionName = storedEntry.sessionName;
|
|
96
|
+
const recordedEpochSeconds = storedEntry.recordedEpochSeconds;
|
|
97
|
+
if (
|
|
98
|
+
typeof sessionName === 'string' &&
|
|
99
|
+
typeof recordedEpochSeconds === 'number' &&
|
|
100
|
+
Number.isFinite(recordedEpochSeconds)
|
|
101
|
+
) {
|
|
102
|
+
entries.push({ sessionName, recordedEpochSeconds });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return entries;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
private writeEntries = (entries: StoredCandidateEntry[]): void => {
|
|
109
|
+
const directory = path.dirname(this.stateFilePath);
|
|
110
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
111
|
+
const temporaryPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
112
|
+
fs.writeFileSync(temporaryPath, JSON.stringify({ candidates: entries }));
|
|
113
|
+
fs.renameSync(temporaryPath, this.stateFilePath);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -912,6 +912,36 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
912
912
|
expect(result).toBeNull();
|
|
913
913
|
});
|
|
914
914
|
|
|
915
|
+
it('should return null when repository is null (NOT_FOUND partial result) instead of throwing', async () => {
|
|
916
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
917
|
+
const repository = new GraphqlProjectItemRepository(
|
|
918
|
+
localStorageRepository,
|
|
919
|
+
'dummy-token',
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
mockPost.mockReturnValueOnce(
|
|
923
|
+
mockJsonResponse({
|
|
924
|
+
data: {
|
|
925
|
+
repository: null,
|
|
926
|
+
},
|
|
927
|
+
errors: [
|
|
928
|
+
{
|
|
929
|
+
type: 'NOT_FOUND',
|
|
930
|
+
path: ['repository'],
|
|
931
|
+
message:
|
|
932
|
+
'Could not resolve to a Repository with the name owner/repo.',
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
}),
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
const result = await repository.fetchProjectItemByUrl(
|
|
939
|
+
'https://github.com/owner/repo/issues/2350',
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
expect(result).toBeNull();
|
|
943
|
+
});
|
|
944
|
+
|
|
915
945
|
const makeMultiProjectPrNode = (
|
|
916
946
|
url: string,
|
|
917
947
|
number: number,
|
|
@@ -986,7 +986,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
986
986
|
repository: {
|
|
987
987
|
issue: ContentNode | null;
|
|
988
988
|
pullRequest: ContentNode | null;
|
|
989
|
-
};
|
|
989
|
+
} | null;
|
|
990
990
|
};
|
|
991
991
|
errors?: { message: string }[];
|
|
992
992
|
}>();
|
|
@@ -999,6 +999,9 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
999
999
|
);
|
|
1000
1000
|
}
|
|
1001
1001
|
const data = response.data;
|
|
1002
|
+
if (!data.repository) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1002
1005
|
const content = data.repository.issue ?? data.repository.pullRequest;
|
|
1003
1006
|
if (!content) {
|
|
1004
1007
|
return null;
|