github-issue-tower-defence-management 1.110.1 → 1.111.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/CHANGELOG.md +7 -0
- package/README.md +8 -7
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +9 -21
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +10 -15
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js +104 -0
- package/bin/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +55 -19
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js +75 -0
- package/bin/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.js.map +1 -0
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +1 -1
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -1
- package/bin/adapter/repositories/ProcFsProcessEnvironReader.js +73 -0
- package/bin/adapter/repositories/ProcFsProcessEnvironReader.js.map +1 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +8 -16
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -1
- package/bin/domain/entities/InteractiveLiveSession.js +3 -0
- package/bin/domain/entities/InteractiveLiveSession.js.map +1 -0
- package/bin/domain/entities/LiveSessionProcessSnapshot.js +3 -0
- package/bin/domain/entities/LiveSessionProcessSnapshot.js.map +1 -0
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +20 -33
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js +89 -0
- package/bin/domain/usecases/ResolveInteractiveLiveSessionsUseCase.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/ProcessEnvironReader.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ProcessEnvironReader.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +5 -15
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +96 -204
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +19 -40
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.test.ts +147 -0
- package/src/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.ts +75 -0
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +82 -44
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +62 -24
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.test.ts +138 -0
- package/src/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.ts +93 -0
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +6 -4
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +1 -1
- package/src/adapter/repositories/ProcFsProcessEnvironReader.test.ts +60 -0
- package/src/adapter/repositories/ProcFsProcessEnvironReader.ts +38 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +61 -81
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +8 -25
- package/src/domain/entities/InteractiveLiveSession.ts +5 -0
- package/src/domain/entities/LiveSessionProcessSnapshot.ts +17 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +125 -349
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +33 -55
- package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.test.ts +220 -0
- package/src/domain/usecases/ResolveInteractiveLiveSessionsUseCase.ts +105 -0
- package/src/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.ts +5 -0
- package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +1 -1
- package/src/domain/usecases/adapter-interfaces/ProcessEnvironReader.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +3 -7
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts +18 -0
- package/types/adapter/repositories/FileSystemInteractiveLiveSessionTranscriptResolver.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +9 -5
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts +21 -0
- package/types/adapter/repositories/LocalProcessLiveSessionProcessSnapshotProvider.d.ts.map +1 -0
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +1 -1
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/adapter/repositories/ProcFsProcessEnvironReader.d.ts +12 -0
- package/types/adapter/repositories/ProcFsProcessEnvironReader.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +2 -4
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/entities/InteractiveLiveSession.d.ts +6 -0
- package/types/domain/entities/InteractiveLiveSession.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionProcessSnapshot.d.ts +16 -0
- package/types/domain/entities/LiveSessionProcessSnapshot.d.ts.map +1 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +6 -11
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts +23 -0
- package/types/domain/usecases/ResolveInteractiveLiveSessionsUseCase.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/InteractiveLiveSessionTranscriptResolver.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/LiveSessionProcessSnapshotProvider.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ProcessEnvironReader.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/ProcessEnvironReader.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -1
|
@@ -1,102 +1,42 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NotifySilentLiveSessionsUseCase,
|
|
3
|
-
DEFAULT_MONITORED_STATUS,
|
|
4
3
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
5
4
|
DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
6
5
|
DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
7
6
|
DEFAULT_NOTIFICATION_COOLDOWN_SECONDS,
|
|
8
7
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS,
|
|
9
8
|
} from './NotifySilentLiveSessionsUseCase';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { LiveSessionProcessSnapshotProvider } from './adapter-interfaces/LiveSessionProcessSnapshotProvider';
|
|
10
|
+
import { InteractiveLiveSessionTranscriptResolver } from './adapter-interfaces/InteractiveLiveSessionTranscriptResolver';
|
|
12
11
|
import { SessionOutputActivityRepository } from './adapter-interfaces/SessionOutputActivityRepository';
|
|
13
12
|
import { SessionSubAgentActivityRepository } from './adapter-interfaces/SessionSubAgentActivityRepository';
|
|
14
13
|
import { OwnerCallStatusProvider } from './adapter-interfaces/OwnerCallStatusProvider';
|
|
15
14
|
import { SilentSessionNotificationRepository } from './adapter-interfaces/SilentSessionNotificationRepository';
|
|
16
15
|
import { SilentSessionMessageComposer } from './adapter-interfaces/SilentSessionMessageComposer';
|
|
17
16
|
import { Sleeper } from './adapter-interfaces/Sleeper';
|
|
18
|
-
import { toTmuxSessionName } from './intmux/InTmuxByHumanSessionReconcileUseCase';
|
|
19
|
-
import { Issue } from '../entities/Issue';
|
|
20
|
-
import { Project } from '../entities/Project';
|
|
21
17
|
import { SubAgentActivity } from '../entities/LiveSessionActivitySnapshot';
|
|
22
|
-
import {
|
|
18
|
+
import { LiveSessionProcessSnapshot } from '../entities/LiveSessionProcessSnapshot';
|
|
19
|
+
import { InteractiveLiveSession } from '../entities/InteractiveLiveSession';
|
|
23
20
|
|
|
24
21
|
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
25
22
|
|
|
26
23
|
const MAIN_STALLED_SECTION = 'MAIN_STALLED_SECTION';
|
|
27
24
|
const SUBAGENT_SECTION = 'SUBAGENT_SECTION';
|
|
28
25
|
|
|
29
|
-
const createMockProject = (): Project => ({
|
|
30
|
-
id: 'project-1',
|
|
31
|
-
url: 'https://github.com/users/user/projects/1',
|
|
32
|
-
databaseId: 1,
|
|
33
|
-
name: 'Test Project',
|
|
34
|
-
status: {
|
|
35
|
-
name: 'Status',
|
|
36
|
-
fieldId: 'field-1',
|
|
37
|
-
statuses: [],
|
|
38
|
-
},
|
|
39
|
-
nextActionDate: null,
|
|
40
|
-
nextActionHour: null,
|
|
41
|
-
story: null,
|
|
42
|
-
remainingEstimationMinutes: null,
|
|
43
|
-
dependedIssueUrlSeparatedByComma: null,
|
|
44
|
-
completionDate50PercentConfidence: null,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
let issueCounter = 0;
|
|
48
|
-
const createMockIssue = (overrides: Partial<Issue> = {}): Issue => {
|
|
49
|
-
issueCounter += 1;
|
|
50
|
-
return {
|
|
51
|
-
nameWithOwner: 'user/repo',
|
|
52
|
-
number: issueCounter,
|
|
53
|
-
title: `Test Issue ${issueCounter}`,
|
|
54
|
-
state: 'OPEN',
|
|
55
|
-
status: IN_TMUX_STATUS_NAME,
|
|
56
|
-
story: null,
|
|
57
|
-
nextActionDate: null,
|
|
58
|
-
nextActionHour: null,
|
|
59
|
-
estimationMinutes: null,
|
|
60
|
-
dependedIssueUrls: [],
|
|
61
|
-
completionDate50PercentConfidence: null,
|
|
62
|
-
url: `https://github.com/user/repo/issues/${issueCounter}`,
|
|
63
|
-
assignees: [],
|
|
64
|
-
labels: [],
|
|
65
|
-
org: 'user',
|
|
66
|
-
repo: 'repo',
|
|
67
|
-
body: '',
|
|
68
|
-
itemId: `item-${issueCounter}`,
|
|
69
|
-
isPr: false,
|
|
70
|
-
isInProgress: false,
|
|
71
|
-
isClosed: false,
|
|
72
|
-
createdAt: new Date(),
|
|
73
|
-
author: 'testuser',
|
|
74
|
-
closingIssueReferenceUrls: [],
|
|
75
|
-
...overrides,
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
26
|
describe('NotifySilentLiveSessionsUseCase', () => {
|
|
80
27
|
let useCase: NotifySilentLiveSessionsUseCase;
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
Pick<TmuxSessionRepository, 'listLiveSessionNames'>
|
|
84
|
-
>;
|
|
28
|
+
let mockSnapshotProvider: Mocked<LiveSessionProcessSnapshotProvider>;
|
|
29
|
+
let mockTranscriptResolver: Mocked<InteractiveLiveSessionTranscriptResolver>;
|
|
85
30
|
let mockSessionOutputActivityRepository: Mocked<SessionOutputActivityRepository>;
|
|
86
31
|
let mockSubAgentActivityRepository: Mocked<SessionSubAgentActivityRepository>;
|
|
87
32
|
let mockOwnerCallStatusProvider: Mocked<OwnerCallStatusProvider>;
|
|
88
33
|
let mockNotificationRepository: Mocked<SilentSessionNotificationRepository>;
|
|
89
34
|
let mockMessageComposer: Mocked<SilentSessionMessageComposer>;
|
|
90
35
|
let mockSleeper: Mocked<Sleeper>;
|
|
91
|
-
let mockProject: Project;
|
|
92
36
|
const now = new Date('2026-06-26T00:00:00Z');
|
|
93
37
|
const nowEpochSeconds = Math.floor(now.getTime() / 1000);
|
|
94
|
-
const allowCacheMinutes = 10;
|
|
95
38
|
|
|
96
39
|
const runParams = (): {
|
|
97
|
-
project: Project;
|
|
98
|
-
allowCacheMinutes: number;
|
|
99
|
-
monitoredStatus: string;
|
|
100
40
|
mainSilentThresholdSeconds: number;
|
|
101
41
|
subAgentSilentThresholdSeconds: number;
|
|
102
42
|
subAgentRunningThresholdSeconds: number;
|
|
@@ -104,9 +44,6 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
104
44
|
staggerSeconds: number;
|
|
105
45
|
now: Date;
|
|
106
46
|
} => ({
|
|
107
|
-
project: mockProject,
|
|
108
|
-
allowCacheMinutes,
|
|
109
|
-
monitoredStatus: DEFAULT_MONITORED_STATUS,
|
|
110
47
|
mainSilentThresholdSeconds: DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
111
48
|
subAgentSilentThresholdSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
112
49
|
subAgentRunningThresholdSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
@@ -115,15 +52,55 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
115
52
|
now,
|
|
116
53
|
});
|
|
117
54
|
|
|
55
|
+
const emptySnapshot: LiveSessionProcessSnapshot = {
|
|
56
|
+
sessions: [],
|
|
57
|
+
processes: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const sessionFor = (sessionName: string): InteractiveLiveSession => ({
|
|
61
|
+
sessionName,
|
|
62
|
+
sessionId: `${sessionName}-uuid`,
|
|
63
|
+
configDir: `/config/${sessionName}`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// The use case derives interactive sessions from the snapshot via the embedded
|
|
67
|
+
// pure resolver. To keep these tests focused on the orchestration, the
|
|
68
|
+
// snapshot is shaped so that the pure resolver yields exactly the named
|
|
69
|
+
// interactive sessions: one pane with a single claude child carrying a session
|
|
70
|
+
// id and config directory.
|
|
71
|
+
const snapshotWithSessions = (
|
|
72
|
+
sessionNames: string[],
|
|
73
|
+
): LiveSessionProcessSnapshot => {
|
|
74
|
+
const sessions = sessionNames.map((sessionName, index) => ({
|
|
75
|
+
sessionName,
|
|
76
|
+
panePids: [100 + index * 10],
|
|
77
|
+
}));
|
|
78
|
+
const processes = sessionNames.map((sessionName, index) => ({
|
|
79
|
+
pid: 101 + index * 10,
|
|
80
|
+
ppid: 100 + index * 10,
|
|
81
|
+
commandLine: `claude --name ${sessionName}`,
|
|
82
|
+
sessionId: `${sessionName}-uuid`,
|
|
83
|
+
configDir: `/config/${sessionName}`,
|
|
84
|
+
}));
|
|
85
|
+
return { sessions, processes };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const transcriptMapFor = (sessionNames: string[]): Map<string, string> =>
|
|
89
|
+
new Map(
|
|
90
|
+
sessionNames.map((sessionName) => [
|
|
91
|
+
sessionName,
|
|
92
|
+
`/config/${sessionName}/projects/-home-user/${sessionName}-uuid.jsonl`,
|
|
93
|
+
]),
|
|
94
|
+
);
|
|
95
|
+
|
|
118
96
|
beforeEach(() => {
|
|
119
97
|
jest.resetAllMocks();
|
|
120
98
|
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
getAllOpened: jest.fn().mockResolvedValue([]),
|
|
99
|
+
mockSnapshotProvider = {
|
|
100
|
+
getSnapshot: jest.fn().mockResolvedValue(emptySnapshot),
|
|
124
101
|
};
|
|
125
|
-
|
|
126
|
-
|
|
102
|
+
mockTranscriptResolver = {
|
|
103
|
+
resolveTranscriptPaths: jest.fn().mockReturnValue(new Map()),
|
|
127
104
|
};
|
|
128
105
|
mockSessionOutputActivityRepository = {
|
|
129
106
|
listSessionOutputActivities: jest.fn().mockResolvedValue([]),
|
|
@@ -153,8 +130,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
153
130
|
sleep: jest.fn().mockResolvedValue(undefined),
|
|
154
131
|
};
|
|
155
132
|
useCase = new NotifySilentLiveSessionsUseCase(
|
|
156
|
-
|
|
157
|
-
|
|
133
|
+
mockSnapshotProvider,
|
|
134
|
+
mockTranscriptResolver,
|
|
158
135
|
mockSessionOutputActivityRepository,
|
|
159
136
|
mockSubAgentActivityRepository,
|
|
160
137
|
mockOwnerCallStatusProvider,
|
|
@@ -164,24 +141,16 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
164
141
|
);
|
|
165
142
|
});
|
|
166
143
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const sessionName = toTmuxSessionName(issue.url);
|
|
175
|
-
mockIssueRepository.getAllOpened.mockResolvedValue([issue]);
|
|
176
|
-
mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
|
|
177
|
-
sessionName,
|
|
178
|
-
]);
|
|
179
|
-
return sessionName;
|
|
144
|
+
const setupLiveInteractiveSession = (sessionName: string): void => {
|
|
145
|
+
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
146
|
+
snapshotWithSessions([sessionName]),
|
|
147
|
+
);
|
|
148
|
+
mockTranscriptResolver.resolveTranscriptPaths.mockReturnValue(
|
|
149
|
+
transcriptMapFor([sessionName]),
|
|
150
|
+
);
|
|
180
151
|
};
|
|
181
152
|
|
|
182
|
-
it('exposes the
|
|
183
|
-
expect(DEFAULT_MONITORED_STATUS).toBe('In Tmux by human');
|
|
184
|
-
expect(DEFAULT_MONITORED_STATUS).toBe(IN_TMUX_STATUS_NAME);
|
|
153
|
+
it('exposes the default thresholds as named constants', () => {
|
|
185
154
|
expect(DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS).toBe(600);
|
|
186
155
|
expect(DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS).toBe(300);
|
|
187
156
|
expect(DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS).toBe(900);
|
|
@@ -189,12 +158,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
189
158
|
expect(DEFAULT_NOTIFICATION_STAGGER_SECONDS).toBe(25);
|
|
190
159
|
});
|
|
191
160
|
|
|
192
|
-
it('
|
|
193
|
-
|
|
161
|
+
it('notifies a plain-named live interactive session independently of any issue', async () => {
|
|
162
|
+
setupLiveInteractiveSession('workbench');
|
|
194
163
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
195
164
|
[
|
|
196
165
|
{
|
|
197
|
-
sessionName,
|
|
166
|
+
sessionName: 'workbench',
|
|
198
167
|
lastOutputEpochSeconds:
|
|
199
168
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
200
169
|
},
|
|
@@ -206,25 +175,41 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
206
175
|
expect(mockMessageComposer.composeMainStalledSection).toHaveBeenCalledWith(
|
|
207
176
|
DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
208
177
|
);
|
|
209
|
-
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
210
178
|
expect(
|
|
211
179
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
212
|
-
).toHaveBeenCalledWith(
|
|
180
|
+
).toHaveBeenCalledWith('workbench', MAIN_STALLED_SECTION);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('passes the resolved transcript paths to the output and owner-call providers', async () => {
|
|
184
|
+
setupLiveInteractiveSession('workbench');
|
|
185
|
+
|
|
186
|
+
await useCase.run(runParams());
|
|
187
|
+
|
|
188
|
+
const expectedMap = transcriptMapFor(['workbench']);
|
|
189
|
+
expect(
|
|
190
|
+
mockSessionOutputActivityRepository.listSessionOutputActivities,
|
|
191
|
+
).toHaveBeenCalledWith(expectedMap);
|
|
192
|
+
expect(
|
|
193
|
+
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall,
|
|
194
|
+
).toHaveBeenCalledWith(expectedMap);
|
|
195
|
+
expect(
|
|
196
|
+
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
|
|
197
|
+
).toHaveBeenCalledWith(['workbench']);
|
|
213
198
|
});
|
|
214
199
|
|
|
215
200
|
it('does not send the main stalled section when an owner call is pending', async () => {
|
|
216
|
-
|
|
201
|
+
setupLiveInteractiveSession('workbench');
|
|
217
202
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
218
203
|
[
|
|
219
204
|
{
|
|
220
|
-
sessionName,
|
|
205
|
+
sessionName: 'workbench',
|
|
221
206
|
lastOutputEpochSeconds:
|
|
222
207
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
223
208
|
},
|
|
224
209
|
],
|
|
225
210
|
);
|
|
226
211
|
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
|
|
227
|
-
new Set([
|
|
212
|
+
new Set(['workbench']),
|
|
228
213
|
);
|
|
229
214
|
|
|
230
215
|
await useCase.run(runParams());
|
|
@@ -238,11 +223,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
238
223
|
});
|
|
239
224
|
|
|
240
225
|
it('does not send the main stalled section when output is within the threshold', async () => {
|
|
241
|
-
|
|
226
|
+
setupLiveInteractiveSession('workbench');
|
|
242
227
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
243
228
|
[
|
|
244
229
|
{
|
|
245
|
-
sessionName,
|
|
230
|
+
sessionName: 'workbench',
|
|
246
231
|
lastOutputEpochSeconds:
|
|
247
232
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS + 1,
|
|
248
233
|
},
|
|
@@ -256,16 +241,8 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
256
241
|
).not.toHaveBeenCalled();
|
|
257
242
|
});
|
|
258
243
|
|
|
259
|
-
it('sends the sub-agent section when a sub-agent exceeds the silent threshold
|
|
260
|
-
|
|
261
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
262
|
-
[
|
|
263
|
-
{
|
|
264
|
-
sessionName,
|
|
265
|
-
lastOutputEpochSeconds: nowEpochSeconds,
|
|
266
|
-
},
|
|
267
|
-
],
|
|
268
|
-
);
|
|
244
|
+
it('sends the sub-agent section when a sub-agent exceeds the silent threshold', async () => {
|
|
245
|
+
setupLiveInteractiveSession('workbench');
|
|
269
246
|
const subAgents: SubAgentActivity[] = [
|
|
270
247
|
{
|
|
271
248
|
label: 'sub-process-1',
|
|
@@ -274,33 +251,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
274
251
|
},
|
|
275
252
|
];
|
|
276
253
|
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
277
|
-
new Map([[
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
await useCase.run(runParams());
|
|
281
|
-
|
|
282
|
-
expect(
|
|
283
|
-
mockMessageComposer.composeMainStalledSection,
|
|
284
|
-
).not.toHaveBeenCalled();
|
|
285
|
-
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
286
|
-
subAgents,
|
|
287
|
-
);
|
|
288
|
-
expect(
|
|
289
|
-
mockNotificationRepository.sendSelfCheckNotification,
|
|
290
|
-
).toHaveBeenCalledWith(sessionName, SUBAGENT_SECTION);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('sends the sub-agent section when a sub-agent exceeds the running threshold', async () => {
|
|
294
|
-
const sessionName = setupLiveMonitoredSession();
|
|
295
|
-
const subAgents: SubAgentActivity[] = [
|
|
296
|
-
{
|
|
297
|
-
label: 'sub-process-1',
|
|
298
|
-
silentSeconds: 10,
|
|
299
|
-
runningSeconds: DEFAULT_SUBAGENT_RUNNING_THRESHOLD_SECONDS,
|
|
300
|
-
},
|
|
301
|
-
];
|
|
302
|
-
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
303
|
-
new Map([[sessionName, subAgents]]),
|
|
254
|
+
new Map([['workbench', subAgents]]),
|
|
304
255
|
);
|
|
305
256
|
|
|
306
257
|
await useCase.run(runParams());
|
|
@@ -308,183 +259,50 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
308
259
|
expect(mockMessageComposer.composeSubAgentSection).toHaveBeenCalledWith(
|
|
309
260
|
subAgents,
|
|
310
261
|
);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('does not include sub-agents that are below both thresholds', async () => {
|
|
314
|
-
const sessionName = setupLiveMonitoredSession();
|
|
315
|
-
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
316
|
-
new Map([
|
|
317
|
-
[
|
|
318
|
-
sessionName,
|
|
319
|
-
[{ label: 'sub-process-1', silentSeconds: 10, runningSeconds: 60 }],
|
|
320
|
-
],
|
|
321
|
-
]),
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
await useCase.run(runParams());
|
|
325
|
-
|
|
326
|
-
expect(mockMessageComposer.composeSubAgentSection).not.toHaveBeenCalled();
|
|
327
262
|
expect(
|
|
328
263
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
329
|
-
).
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('combines both sections into a single notification when both classifications apply', async () => {
|
|
333
|
-
const sessionName = setupLiveMonitoredSession();
|
|
334
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
335
|
-
[
|
|
336
|
-
{
|
|
337
|
-
sessionName,
|
|
338
|
-
lastOutputEpochSeconds:
|
|
339
|
-
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
340
|
-
},
|
|
341
|
-
],
|
|
342
|
-
);
|
|
343
|
-
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
344
|
-
new Map([
|
|
345
|
-
[
|
|
346
|
-
sessionName,
|
|
347
|
-
[
|
|
348
|
-
{
|
|
349
|
-
label: 'sub-process-1',
|
|
350
|
-
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
351
|
-
runningSeconds: 60,
|
|
352
|
-
},
|
|
353
|
-
],
|
|
354
|
-
],
|
|
355
|
-
]),
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
await useCase.run(runParams());
|
|
359
|
-
|
|
360
|
-
const sendCall =
|
|
361
|
-
mockNotificationRepository.sendSelfCheckNotification.mock.calls[0];
|
|
362
|
-
expect(sendCall[0]).toBe(sessionName);
|
|
363
|
-
expect(sendCall[1]).toContain(MAIN_STALLED_SECTION);
|
|
364
|
-
expect(sendCall[1]).toContain(SUBAGENT_SECTION);
|
|
264
|
+
).toHaveBeenCalledWith('workbench', SUBAGENT_SECTION);
|
|
365
265
|
});
|
|
366
266
|
|
|
367
|
-
it('
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
[
|
|
267
|
+
it('excludes an owner-handover spawn from selection so no notification is sent', async () => {
|
|
268
|
+
mockSnapshotProvider.getSnapshot.mockResolvedValue({
|
|
269
|
+
sessions: [{ sessionName: 'aw-host', panePids: [100] }],
|
|
270
|
+
processes: [
|
|
371
271
|
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
272
|
+
pid: 101,
|
|
273
|
+
ppid: 100,
|
|
274
|
+
commandLine:
|
|
275
|
+
'claude --verbose -p Take ownership of https://example.com/issues/1 and finish it',
|
|
276
|
+
sessionId: 'aw-uuid',
|
|
277
|
+
configDir: '/config/aw',
|
|
375
278
|
},
|
|
376
279
|
],
|
|
377
|
-
);
|
|
378
|
-
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall.mockResolvedValue(
|
|
379
|
-
new Set([sessionName]),
|
|
380
|
-
);
|
|
381
|
-
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName.mockResolvedValue(
|
|
382
|
-
new Map([
|
|
383
|
-
[
|
|
384
|
-
sessionName,
|
|
385
|
-
[
|
|
386
|
-
{
|
|
387
|
-
label: 'sub-process-1',
|
|
388
|
-
silentSeconds: DEFAULT_SUBAGENT_SILENT_THRESHOLD_SECONDS,
|
|
389
|
-
runningSeconds: 60,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
],
|
|
393
|
-
]),
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
await useCase.run(runParams());
|
|
397
|
-
|
|
398
|
-
const sendCall =
|
|
399
|
-
mockNotificationRepository.sendSelfCheckNotification.mock.calls[0];
|
|
400
|
-
expect(sendCall[1]).not.toContain(MAIN_STALLED_SECTION);
|
|
401
|
-
expect(sendCall[1]).toContain(SUBAGENT_SECTION);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('passes the monitored session names to every data provider', async () => {
|
|
405
|
-
const sessionName = setupLiveMonitoredSession();
|
|
280
|
+
});
|
|
406
281
|
|
|
407
282
|
await useCase.run(runParams());
|
|
408
283
|
|
|
409
|
-
expect(
|
|
410
|
-
|
|
411
|
-
).toHaveBeenCalledWith([sessionName]);
|
|
412
|
-
expect(
|
|
413
|
-
mockSubAgentActivityRepository.listSubAgentActivitiesBySessionName,
|
|
414
|
-
).toHaveBeenCalledWith([sessionName]);
|
|
415
|
-
expect(
|
|
416
|
-
mockOwnerCallStatusProvider.listSessionNamesWithUnansweredOwnerCall,
|
|
417
|
-
).toHaveBeenCalledWith([sessionName]);
|
|
418
|
-
expect(mockIssueRepository.getAllOpened).toHaveBeenCalledWith(
|
|
419
|
-
mockProject,
|
|
420
|
-
allowCacheMinutes,
|
|
284
|
+
expect(mockTranscriptResolver.resolveTranscriptPaths).toHaveBeenCalledWith(
|
|
285
|
+
[],
|
|
421
286
|
);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('does not notify a live session that maps to no monitored issue', async () => {
|
|
425
|
-
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
426
|
-
mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
|
|
427
|
-
'orphan_session',
|
|
428
|
-
]);
|
|
429
|
-
|
|
430
|
-
await useCase.run(runParams());
|
|
431
|
-
|
|
432
|
-
expect(
|
|
433
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities,
|
|
434
|
-
).toHaveBeenCalledWith([]);
|
|
435
|
-
expect(
|
|
436
|
-
mockNotificationRepository.sendSelfCheckNotification,
|
|
437
|
-
).not.toHaveBeenCalled();
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('does not notify a monitored issue whose session is not live', async () => {
|
|
441
|
-
const issue = createMockIssue({ status: IN_TMUX_STATUS_NAME });
|
|
442
|
-
mockIssueRepository.getAllOpened.mockResolvedValue([issue]);
|
|
443
|
-
mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([]);
|
|
444
|
-
|
|
445
|
-
await useCase.run(runParams());
|
|
446
|
-
|
|
447
|
-
expect(
|
|
448
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities,
|
|
449
|
-
).toHaveBeenCalledWith([]);
|
|
450
287
|
expect(
|
|
451
288
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
452
289
|
).not.toHaveBeenCalled();
|
|
453
290
|
});
|
|
454
291
|
|
|
455
|
-
it('does
|
|
456
|
-
const issue = createMockIssue({ status: 'In Progress' });
|
|
457
|
-
const sessionName = toTmuxSessionName(issue.url);
|
|
458
|
-
mockIssueRepository.getAllOpened.mockResolvedValue([issue]);
|
|
459
|
-
mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
|
|
460
|
-
sessionName,
|
|
461
|
-
]);
|
|
462
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
463
|
-
[
|
|
464
|
-
{
|
|
465
|
-
sessionName,
|
|
466
|
-
lastOutputEpochSeconds:
|
|
467
|
-
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
);
|
|
471
|
-
|
|
292
|
+
it('does nothing when there are no live interactive sessions', async () => {
|
|
472
293
|
await useCase.run(runParams());
|
|
473
294
|
|
|
474
|
-
expect(
|
|
475
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities,
|
|
476
|
-
).toHaveBeenCalledWith([]);
|
|
477
295
|
expect(
|
|
478
296
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
479
297
|
).not.toHaveBeenCalled();
|
|
480
298
|
});
|
|
481
299
|
|
|
482
300
|
it('does not re-notify a session within the cooldown window', async () => {
|
|
483
|
-
|
|
301
|
+
setupLiveInteractiveSession('workbench');
|
|
484
302
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
485
303
|
[
|
|
486
304
|
{
|
|
487
|
-
sessionName,
|
|
305
|
+
sessionName: 'workbench',
|
|
488
306
|
lastOutputEpochSeconds:
|
|
489
307
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
490
308
|
},
|
|
@@ -499,68 +317,24 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
499
317
|
expect(
|
|
500
318
|
mockNotificationRepository.sendSelfCheckNotification,
|
|
501
319
|
).not.toHaveBeenCalled();
|
|
502
|
-
expect(
|
|
503
|
-
mockNotificationRepository.setLastNotifiedEpochSeconds,
|
|
504
|
-
).not.toHaveBeenCalled();
|
|
505
320
|
});
|
|
506
321
|
|
|
507
|
-
it('
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
[
|
|
511
|
-
{
|
|
512
|
-
sessionName,
|
|
513
|
-
lastOutputEpochSeconds:
|
|
514
|
-
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
515
|
-
},
|
|
516
|
-
],
|
|
322
|
+
it('sends to multiple sessions sequentially with a stagger delay between sends', async () => {
|
|
323
|
+
mockSnapshotProvider.getSnapshot.mockResolvedValue(
|
|
324
|
+
snapshotWithSessions(['alpha', 'bravo']),
|
|
517
325
|
);
|
|
518
|
-
|
|
519
|
-
|
|
326
|
+
mockTranscriptResolver.resolveTranscriptPaths.mockReturnValue(
|
|
327
|
+
transcriptMapFor(['alpha', 'bravo']),
|
|
520
328
|
);
|
|
521
|
-
|
|
522
|
-
await useCase.run(runParams());
|
|
523
|
-
|
|
524
|
-
expect(
|
|
525
|
-
mockNotificationRepository.sendSelfCheckNotification,
|
|
526
|
-
).toHaveBeenCalledWith(sessionName, MAIN_STALLED_SECTION);
|
|
527
|
-
expect(
|
|
528
|
-
mockNotificationRepository.setLastNotifiedEpochSeconds,
|
|
529
|
-
).toHaveBeenCalledWith(sessionName, nowEpochSeconds);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('does not notify a session that has no recorded output activity and no sub-agents', async () => {
|
|
533
|
-
setupLiveMonitoredSession();
|
|
534
|
-
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
535
|
-
[],
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
await useCase.run(runParams());
|
|
539
|
-
|
|
540
|
-
expect(
|
|
541
|
-
mockNotificationRepository.sendSelfCheckNotification,
|
|
542
|
-
).not.toHaveBeenCalled();
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it('sends to multiple sessions sequentially with a stagger delay between sends, not before the first or after the last', async () => {
|
|
546
|
-
const issueA = createMockIssue({ status: IN_TMUX_STATUS_NAME });
|
|
547
|
-
const issueB = createMockIssue({ status: IN_TMUX_STATUS_NAME });
|
|
548
|
-
const sessionA = toTmuxSessionName(issueA.url);
|
|
549
|
-
const sessionB = toTmuxSessionName(issueB.url);
|
|
550
|
-
mockIssueRepository.getAllOpened.mockResolvedValue([issueA, issueB]);
|
|
551
|
-
mockTmuxSessionRepository.listLiveSessionNames.mockResolvedValue([
|
|
552
|
-
sessionA,
|
|
553
|
-
sessionB,
|
|
554
|
-
]);
|
|
555
329
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
556
330
|
[
|
|
557
331
|
{
|
|
558
|
-
sessionName:
|
|
332
|
+
sessionName: 'alpha',
|
|
559
333
|
lastOutputEpochSeconds:
|
|
560
334
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
561
335
|
},
|
|
562
336
|
{
|
|
563
|
-
sessionName:
|
|
337
|
+
sessionName: 'bravo',
|
|
564
338
|
lastOutputEpochSeconds:
|
|
565
339
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
566
340
|
},
|
|
@@ -579,13 +353,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
579
353
|
|
|
580
354
|
await useCase.run(runParams());
|
|
581
355
|
|
|
582
|
-
|
|
583
|
-
const sortedSecond = [sessionA, sessionB].sort()[1];
|
|
584
|
-
expect(callOrder).toEqual([
|
|
585
|
-
`send:${sortedFirst}`,
|
|
586
|
-
'sleep',
|
|
587
|
-
`send:${sortedSecond}`,
|
|
588
|
-
]);
|
|
356
|
+
expect(callOrder).toEqual(['send:alpha', 'sleep', 'send:bravo']);
|
|
589
357
|
expect(mockSleeper.sleep).toHaveBeenCalledTimes(1);
|
|
590
358
|
expect(mockSleeper.sleep).toHaveBeenCalledWith(
|
|
591
359
|
DEFAULT_NOTIFICATION_STAGGER_SECONDS * 1000,
|
|
@@ -593,11 +361,11 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
593
361
|
});
|
|
594
362
|
|
|
595
363
|
it('does not suppress errors raised while sending a notification', async () => {
|
|
596
|
-
|
|
364
|
+
setupLiveInteractiveSession('workbench');
|
|
597
365
|
mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
|
|
598
366
|
[
|
|
599
367
|
{
|
|
600
|
-
sessionName,
|
|
368
|
+
sessionName: 'workbench',
|
|
601
369
|
lastOutputEpochSeconds:
|
|
602
370
|
nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
|
|
603
371
|
},
|
|
@@ -609,4 +377,12 @@ describe('NotifySilentLiveSessionsUseCase', () => {
|
|
|
609
377
|
|
|
610
378
|
await expect(useCase.run(runParams())).rejects.toThrow('send-keys failed');
|
|
611
379
|
});
|
|
380
|
+
|
|
381
|
+
it('uses the session entity helper for type completeness', () => {
|
|
382
|
+
expect(sessionFor('workbench')).toEqual({
|
|
383
|
+
sessionName: 'workbench',
|
|
384
|
+
sessionId: 'workbench-uuid',
|
|
385
|
+
configDir: '/config/workbench',
|
|
386
|
+
});
|
|
387
|
+
});
|
|
612
388
|
});
|