github-issue-tower-defence-management 1.109.2 → 1.110.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 +19 -0
- package/README.md +26 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +125 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js +2 -0
- package/bin/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js +2 -0
- package/bin/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js +2 -0
- package/bin/adapter/entry-points/handlers/OauthTokenSelectHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +62 -0
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +88 -3
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/proxy/RateLimitCache.js +33 -1
- package/bin/adapter/proxy/RateLimitCache.js.map +1 -1
- package/bin/adapter/proxy/proxyEntry.js +25 -0
- package/bin/adapter/proxy/proxyEntry.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +37 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js +76 -0
- package/bin/adapter/repositories/FileSystemSessionOutputActivityRepository.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js +67 -0
- package/bin/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.js.map +1 -0
- package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js +50 -0
- package/bin/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.js.map +1 -0
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js +12 -0
- package/bin/adapter/repositories/NoUnansweredOwnerCallStatusProvider.js.map +1 -0
- package/bin/adapter/repositories/NodeSubAgentProcessLister.js +37 -0
- package/bin/adapter/repositories/NodeSubAgentProcessLister.js.map +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +48 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -0
- package/bin/adapter/repositories/RealSleeper.js +17 -0
- package/bin/adapter/repositories/RealSleeper.js.map +1 -0
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +56 -0
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js +150 -0
- package/bin/adapter/repositories/TranscriptOwnerCallStatusProvider.js.map +1 -0
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +152 -0
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -0
- package/bin/domain/entities/LiveSessionActivitySnapshot.js +3 -0
- package/bin/domain/entities/LiveSessionActivitySnapshot.js.map +1 -0
- package/bin/domain/entities/LiveSessionOutputActivity.js +3 -0
- package/bin/domain/entities/LiveSessionOutputActivity.js.map +1 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +35 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -0
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +113 -0
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -0
- package/bin/domain/usecases/OauthTokenSelectUseCase.js +8 -2
- package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/Sleeper.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/Sleeper.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentProcessLister.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.js.map +1 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.stories.tsx +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.test.tsx +63 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleCopyUrlButton.tsx +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.test.tsx +7 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsoleItemDetail.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/index.css +14 -0
- package/src/adapter/entry-points/console/ui-dist/assets/{index-CO-f_j4f.css → index-BPH6W1yn.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-CT3t4Yje.js +101 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +122 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.ts +2 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +112 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.ts +2 -0
- package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.test.ts +131 -0
- package/src/adapter/entry-points/handlers/OauthTokenSelectHandler.ts +2 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +338 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +174 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +79 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +117 -3
- package/src/adapter/proxy/RateLimitCache.test.ts +152 -0
- package/src/adapter/proxy/RateLimitCache.ts +42 -0
- package/src/adapter/proxy/proxyEntry.test.ts +92 -0
- package/src/adapter/proxy/proxyEntry.ts +32 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +77 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +51 -0
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.test.ts +92 -0
- package/src/adapter/repositories/FileSystemSessionOutputActivityRepository.ts +48 -0
- package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.test.ts +56 -0
- package/src/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.ts +32 -0
- package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.test.ts +32 -0
- package/src/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.ts +17 -0
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.test.ts +12 -0
- package/src/adapter/repositories/NoUnansweredOwnerCallStatusProvider.ts +9 -0
- package/src/adapter/repositories/NodeSubAgentProcessLister.test.ts +43 -0
- package/src/adapter/repositories/NodeSubAgentProcessLister.ts +40 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +109 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +56 -0
- package/src/adapter/repositories/RealSleeper.test.ts +19 -0
- package/src/adapter/repositories/RealSleeper.ts +12 -0
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +153 -0
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +83 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +220 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.ts +139 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +217 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +145 -0
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +12 -0
- package/src/domain/entities/LiveSessionOutputActivity.ts +4 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +52 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +39 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +2 -0
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +40 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +612 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +223 -0
- package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +36 -0
- package/src/domain/usecases/OauthTokenSelectUseCase.ts +12 -0
- package/src/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.ts +5 -0
- package/src/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.ts +7 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +6 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.ts +11 -0
- package/src/domain/usecases/adapter-interfaces/Sleeper.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentProcessLister.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/OauthTokenSelectHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +34 -0
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts +1 -0
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/proxy/RateLimitCache.d.ts +3 -0
- package/types/adapter/proxy/RateLimitCache.d.ts.map +1 -1
- package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +15 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemSessionOutputActivityRepository.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts +9 -0
- package/types/adapter/repositories/FileSystemSubAgentSilentSecondsResolver.d.ts.map +1 -0
- package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts +7 -0
- package/types/adapter/repositories/FileSystemSubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts +5 -0
- package/types/adapter/repositories/NoUnansweredOwnerCallStatusProvider.d.ts.map +1 -0
- package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts +8 -0
- package/types/adapter/repositories/NodeSubAgentProcessLister.d.ts.map +1 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts +12 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -0
- package/types/adapter/repositories/RealSleeper.d.ts +5 -0
- package/types/adapter/repositories/RealSleeper.d.ts.map +1 -0
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts +13 -0
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts +10 -0
- package/types/adapter/repositories/TranscriptOwnerCallStatusProvider.d.ts.map +1 -0
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +12 -0
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +12 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionOutputActivity.d.ts +5 -0
- package/types/domain/entities/LiveSessionOutputActivity.d.ts.map +1 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +7 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +41 -0
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -0
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +2 -0
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/OwnerCallStatusProvider.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/SessionOutputActivityRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts +5 -0
- package/types/domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +6 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts +6 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionNotificationRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/Sleeper.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentProcessLister.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentSilentSecondsResolver.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver.d.ts.map +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
- package/src/adapter/entry-points/console/ui-dist/assets/index-BpccigBS.js +0 -101
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
5
|
+
import { TranscriptSessionSubAgentActivityRepository } from './TranscriptSessionSubAgentActivityRepository';
|
|
6
|
+
|
|
7
|
+
describe('TranscriptSessionSubAgentActivityRepository', () => {
|
|
8
|
+
let rootDirectory: string;
|
|
9
|
+
const now = new Date('2026-06-27T12:00:00.000Z');
|
|
10
|
+
const nowEpochSeconds = Math.floor(now.getTime() / 1000);
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
rootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'subagent-tx-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(rootDirectory, { force: true, recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const subAgentsDirFor = (sessionName: string): string => {
|
|
21
|
+
const dir = path.join(
|
|
22
|
+
rootDirectory,
|
|
23
|
+
sessionName.replace(/\//g, '_'),
|
|
24
|
+
'subagents',
|
|
25
|
+
);
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
return dir;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const writeAgentTranscript = (
|
|
31
|
+
sessionName: string,
|
|
32
|
+
agentId: string,
|
|
33
|
+
entries: object[],
|
|
34
|
+
mtimeEpochSeconds: number,
|
|
35
|
+
): string => {
|
|
36
|
+
const dir = subAgentsDirFor(sessionName);
|
|
37
|
+
const filePath = path.join(dir, `agent-${agentId}.jsonl`);
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
filePath,
|
|
40
|
+
entries.map((entry) => JSON.stringify(entry)).join('\n'),
|
|
41
|
+
'utf8',
|
|
42
|
+
);
|
|
43
|
+
fs.utimesSync(filePath, mtimeEpochSeconds, mtimeEpochSeconds);
|
|
44
|
+
return filePath;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const createResolver = (): SubAgentTranscriptDirectoryResolver => ({
|
|
48
|
+
resolveSubAgentsDirectory: (sessionName) =>
|
|
49
|
+
path.join(rootDirectory, sessionName.replace(/\//g, '_'), 'subagents'),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const runningEntries = (startTimestamp: string): object[] => [
|
|
53
|
+
{ type: 'user', timestamp: startTimestamp, message: { role: 'user' } },
|
|
54
|
+
{
|
|
55
|
+
type: 'assistant',
|
|
56
|
+
timestamp: startTimestamp,
|
|
57
|
+
message: { role: 'assistant', stop_reason: 'tool_use' },
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const finishedEntries = (startTimestamp: string): object[] => [
|
|
62
|
+
{ type: 'user', timestamp: startTimestamp, message: { role: 'user' } },
|
|
63
|
+
{
|
|
64
|
+
type: 'assistant',
|
|
65
|
+
timestamp: startTimestamp,
|
|
66
|
+
message: { role: 'assistant', stop_reason: 'end_turn' },
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
it('reports a running sub-agent with silent seconds from the file mtime and running seconds from the first entry', async () => {
|
|
71
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
72
|
+
const startTimestamp = '2026-06-27T11:45:00.000Z';
|
|
73
|
+
writeAgentTranscript(
|
|
74
|
+
sessionName,
|
|
75
|
+
'aaa111',
|
|
76
|
+
runningEntries(startTimestamp),
|
|
77
|
+
nowEpochSeconds - 120,
|
|
78
|
+
);
|
|
79
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
80
|
+
createResolver(),
|
|
81
|
+
now,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
85
|
+
sessionName,
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
expect(result.get(sessionName)).toEqual([
|
|
89
|
+
{ label: 'agent-aaa111', silentSeconds: 120, runningSeconds: 900 },
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('excludes a finished sub-agent whose last entry stop_reason is end_turn', async () => {
|
|
94
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
95
|
+
writeAgentTranscript(
|
|
96
|
+
sessionName,
|
|
97
|
+
'done1',
|
|
98
|
+
finishedEntries('2026-06-27T11:00:00.000Z'),
|
|
99
|
+
nowEpochSeconds - 30,
|
|
100
|
+
);
|
|
101
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
102
|
+
createResolver(),
|
|
103
|
+
now,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
107
|
+
sessionName,
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
expect(result.size).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('follows a symlinked transcript when computing silent seconds', async () => {
|
|
114
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
115
|
+
const targetDir = fs.mkdtempSync(
|
|
116
|
+
path.join(os.tmpdir(), 'subagent-target-'),
|
|
117
|
+
);
|
|
118
|
+
const targetPath = path.join(targetDir, 'real-agent.jsonl');
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
targetPath,
|
|
121
|
+
runningEntries('2026-06-27T11:50:00.000Z')
|
|
122
|
+
.map((entry) => JSON.stringify(entry))
|
|
123
|
+
.join('\n'),
|
|
124
|
+
'utf8',
|
|
125
|
+
);
|
|
126
|
+
fs.utimesSync(targetPath, nowEpochSeconds - 60, nowEpochSeconds - 60);
|
|
127
|
+
const dir = subAgentsDirFor(sessionName);
|
|
128
|
+
fs.symlinkSync(targetPath, path.join(dir, 'agent-link1.jsonl'));
|
|
129
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
130
|
+
createResolver(),
|
|
131
|
+
now,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
135
|
+
sessionName,
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
expect(result.get(sessionName)).toEqual([
|
|
139
|
+
{ label: 'agent-link1', silentSeconds: 60, runningSeconds: 600 },
|
|
140
|
+
]);
|
|
141
|
+
fs.rmSync(targetDir, { force: true, recursive: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns an empty map when the resolver returns null', async () => {
|
|
145
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
146
|
+
{ resolveSubAgentsDirectory: () => null },
|
|
147
|
+
now,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
151
|
+
'https_//github_com/owner/repo/issues/9',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
expect(result.size).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('ignores a session whose subagents directory does not exist', async () => {
|
|
158
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
159
|
+
createResolver(),
|
|
160
|
+
now,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
164
|
+
'https_//github_com/owner/repo/issues/404',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
expect(result.size).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('groups multiple running sub-agents under the same session', async () => {
|
|
171
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
172
|
+
writeAgentTranscript(
|
|
173
|
+
sessionName,
|
|
174
|
+
'one',
|
|
175
|
+
runningEntries('2026-06-27T11:55:00.000Z'),
|
|
176
|
+
nowEpochSeconds - 10,
|
|
177
|
+
);
|
|
178
|
+
writeAgentTranscript(
|
|
179
|
+
sessionName,
|
|
180
|
+
'two',
|
|
181
|
+
runningEntries('2026-06-27T11:55:00.000Z'),
|
|
182
|
+
nowEpochSeconds - 20,
|
|
183
|
+
);
|
|
184
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
185
|
+
createResolver(),
|
|
186
|
+
now,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
190
|
+
sessionName,
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
expect(result.get(sessionName)).toHaveLength(2);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('clamps negative silent and running seconds to zero', async () => {
|
|
197
|
+
const sessionName = 'https_//github_com/owner/repo/issues/9';
|
|
198
|
+
writeAgentTranscript(
|
|
199
|
+
sessionName,
|
|
200
|
+
'future',
|
|
201
|
+
runningEntries('2026-06-27T13:00:00.000Z'),
|
|
202
|
+
nowEpochSeconds + 100,
|
|
203
|
+
);
|
|
204
|
+
const repository = new TranscriptSessionSubAgentActivityRepository(
|
|
205
|
+
createResolver(),
|
|
206
|
+
now,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const result = await repository.listSubAgentActivitiesBySessionName([
|
|
210
|
+
sessionName,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
expect(result.get(sessionName)).toEqual([
|
|
214
|
+
{ label: 'agent-future', silentSeconds: 0, runningSeconds: 0 },
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { SubAgentActivity } from '../../domain/entities/LiveSessionActivitySnapshot';
|
|
4
|
+
import { SessionSubAgentActivityRepository } from '../../domain/usecases/adapter-interfaces/SessionSubAgentActivityRepository';
|
|
5
|
+
import { SubAgentTranscriptDirectoryResolver } from '../../domain/usecases/adapter-interfaces/SubAgentTranscriptDirectoryResolver';
|
|
6
|
+
|
|
7
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
8
|
+
typeof value === 'object' && value !== null;
|
|
9
|
+
|
|
10
|
+
const readString = (
|
|
11
|
+
value: Record<string, unknown>,
|
|
12
|
+
key: string,
|
|
13
|
+
): string | null => {
|
|
14
|
+
const candidate = value[key];
|
|
15
|
+
return typeof candidate === 'string' ? candidate : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const parseEpochSeconds = (timestamp: string | null): number | null => {
|
|
19
|
+
if (timestamp === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const parsed = Date.parse(timestamp);
|
|
23
|
+
return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ParsedTranscript = {
|
|
27
|
+
firstEntryEpochSeconds: number | null;
|
|
28
|
+
lastStopReason: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const parseTranscript = (content: string): ParsedTranscript => {
|
|
32
|
+
let firstEntryEpochSeconds: number | null = null;
|
|
33
|
+
let lastStopReason: string | null = null;
|
|
34
|
+
for (const line of content.split('\n')) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (trimmed.length === 0) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let parsed: unknown;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(trimmed);
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!isRecord(parsed)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const epochSeconds = parseEpochSeconds(readString(parsed, 'timestamp'));
|
|
49
|
+
if (firstEntryEpochSeconds === null && epochSeconds !== null) {
|
|
50
|
+
firstEntryEpochSeconds = epochSeconds;
|
|
51
|
+
}
|
|
52
|
+
const message = parsed.message;
|
|
53
|
+
if (isRecord(message)) {
|
|
54
|
+
const stopReason = readString(message, 'stop_reason');
|
|
55
|
+
if (stopReason !== null) {
|
|
56
|
+
lastStopReason = stopReason;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { firstEntryEpochSeconds, lastStopReason };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const clampToZero = (value: number): number => (value > 0 ? value : 0);
|
|
64
|
+
|
|
65
|
+
export class TranscriptSessionSubAgentActivityRepository implements SessionSubAgentActivityRepository {
|
|
66
|
+
constructor(
|
|
67
|
+
private readonly directoryResolver: SubAgentTranscriptDirectoryResolver,
|
|
68
|
+
private readonly now: Date,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
listSubAgentActivitiesBySessionName = async (
|
|
72
|
+
sessionNames: string[],
|
|
73
|
+
): Promise<Map<string, SubAgentActivity[]>> => {
|
|
74
|
+
const result = new Map<string, SubAgentActivity[]>();
|
|
75
|
+
const nowEpochSeconds = Math.floor(this.now.getTime() / 1000);
|
|
76
|
+
for (const sessionName of sessionNames) {
|
|
77
|
+
const directory =
|
|
78
|
+
this.directoryResolver.resolveSubAgentsDirectory(sessionName);
|
|
79
|
+
if (directory === null) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const activities = this.collectActivities(directory, nowEpochSeconds);
|
|
83
|
+
if (activities.length > 0) {
|
|
84
|
+
result.set(sessionName, activities);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
private collectActivities = (
|
|
91
|
+
directory: string,
|
|
92
|
+
nowEpochSeconds: number,
|
|
93
|
+
): SubAgentActivity[] => {
|
|
94
|
+
let entries: fs.Dirent[];
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const activities: SubAgentActivity[] = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const fileName = entry.name;
|
|
103
|
+
if (!fileName.startsWith('agent-') || !fileName.endsWith('.jsonl')) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const filePath = path.join(directory, fileName);
|
|
107
|
+
const activity = this.toActivity(filePath, fileName, nowEpochSeconds);
|
|
108
|
+
if (activity !== null) {
|
|
109
|
+
activities.push(activity);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return activities;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
private toActivity = (
|
|
116
|
+
filePath: string,
|
|
117
|
+
fileName: string,
|
|
118
|
+
nowEpochSeconds: number,
|
|
119
|
+
): SubAgentActivity | null => {
|
|
120
|
+
let content: string;
|
|
121
|
+
let stats: fs.Stats;
|
|
122
|
+
try {
|
|
123
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
124
|
+
stats = fs.statSync(filePath);
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const transcript = parseTranscript(content);
|
|
129
|
+
if (transcript.lastStopReason === 'end_turn') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const silentSeconds = clampToZero(
|
|
133
|
+
nowEpochSeconds - Math.floor(stats.mtimeMs / 1000),
|
|
134
|
+
);
|
|
135
|
+
const runningSeconds =
|
|
136
|
+
transcript.firstEntryEpochSeconds === null
|
|
137
|
+
? 0
|
|
138
|
+
: clampToZero(nowEpochSeconds - transcript.firstEntryEpochSeconds);
|
|
139
|
+
return {
|
|
140
|
+
label: fileName.replace(/\.jsonl$/, ''),
|
|
141
|
+
silentSeconds,
|
|
142
|
+
runningSeconds,
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SubAgentActivity = {
|
|
2
|
+
label: string;
|
|
3
|
+
silentSeconds: number;
|
|
4
|
+
runningSeconds: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type LiveSessionActivitySnapshot = {
|
|
8
|
+
sessionName: string;
|
|
9
|
+
mainSilentSeconds: number | null;
|
|
10
|
+
subAgents: SubAgentActivity[];
|
|
11
|
+
hasUnansweredOwnerCall: boolean;
|
|
12
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DefaultSilentSessionMessageComposer } from './DefaultSilentSessionMessageComposer';
|
|
2
|
+
|
|
3
|
+
describe('DefaultSilentSessionMessageComposer', () => {
|
|
4
|
+
const composer = new DefaultSilentSessionMessageComposer();
|
|
5
|
+
|
|
6
|
+
it('renders the configured main-stalled message with the silent minutes substituted', () => {
|
|
7
|
+
const section = composer.composeMainStalledSection(600);
|
|
8
|
+
expect(section).toContain('You have produced no output for 10 minutes.');
|
|
9
|
+
expect(section).toContain('Idle waiting wastes this live session');
|
|
10
|
+
expect(section).toContain(
|
|
11
|
+
'Always work proactively and stay ahead of the work',
|
|
12
|
+
);
|
|
13
|
+
expect(section).toContain('never wait passively');
|
|
14
|
+
expect(section).toContain('Self-check now:');
|
|
15
|
+
expect(section).toContain('1.');
|
|
16
|
+
expect(section).toContain('2.');
|
|
17
|
+
expect(section).toContain('3.');
|
|
18
|
+
expect(section).toContain('4.');
|
|
19
|
+
expect(section).toContain(
|
|
20
|
+
'report an estimate of how many minutes you expect to need',
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('floors the silent seconds to whole minutes', () => {
|
|
25
|
+
const section = composer.composeMainStalledSection(659);
|
|
26
|
+
expect(section).toContain('You have produced no output for 10 minutes.');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('lists each sub-agent with its silent and running minutes', () => {
|
|
30
|
+
const section = composer.composeSubAgentSection([
|
|
31
|
+
{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
|
|
32
|
+
{ label: 'sub-process-2', silentSeconds: 0, runningSeconds: 960 },
|
|
33
|
+
]);
|
|
34
|
+
expect(section).toContain('sub-process-1');
|
|
35
|
+
expect(section).toContain('silent for 6m');
|
|
36
|
+
expect(section).toContain('running for 20m');
|
|
37
|
+
expect(section).toContain('sub-process-2');
|
|
38
|
+
expect(section).toContain('running for 16m');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('does not contain any host-specific or internal identifiers', () => {
|
|
42
|
+
const mainSection = composer.composeMainStalledSection(600);
|
|
43
|
+
const subSection = composer.composeSubAgentSection([
|
|
44
|
+
{ label: 'sub-process-1', silentSeconds: 360, runningSeconds: 1200 },
|
|
45
|
+
]);
|
|
46
|
+
const combined = `${mainSection}\n${subSection}`.toLowerCase();
|
|
47
|
+
expect(combined).not.toContain('claude');
|
|
48
|
+
expect(combined).not.toContain('take ownership');
|
|
49
|
+
expect(combined).not.toContain('/home/');
|
|
50
|
+
expect(combined).not.toContain('.jsonl');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SubAgentActivity } from '../entities/LiveSessionActivitySnapshot';
|
|
2
|
+
import { SilentSessionMessageComposer } from './adapter-interfaces/SilentSessionMessageComposer';
|
|
3
|
+
|
|
4
|
+
const formatMinutes = (seconds: number): string => {
|
|
5
|
+
const minutes = Math.floor(seconds / 60);
|
|
6
|
+
return `${minutes}m`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const composeMainStalledMessage = (mainSilentSeconds: number): string => {
|
|
10
|
+
const minutes = Math.floor(mainSilentSeconds / 60);
|
|
11
|
+
return [
|
|
12
|
+
`You have produced no output for ${minutes} minutes. Idle waiting wastes this live session and is unacceptable. Always work proactively and stay ahead of the work: anticipate the next steps and, at every point, choose the fastest path so the whole task finishes as early as possible; never wait passively. Finish every task in the shortest possible time — but "fastest" means correct and incident-free, not merely quick: a fast but wrong result is worthless and causes incidents, so be fast without breaking things. Use parallel execution and your whole team of sub-agents to minimize total wall-clock time. Your goal is to drive every task to completion and have the owner confirm that all tasks are done. Do NOT close this session on your own — it is closed only after the owner has verified completion and tells you to close it. Self-check now:`,
|
|
13
|
+
`1. Every request from the owner is registered as a session task and your task list is kept current (mark tasks completed when done); verify nothing is missing or stale.`,
|
|
14
|
+
`2. Your plan is the fastest correct path: parallelize independent work across sub-agents, delegate, and remove needless serialization. Choose the fastest safe method, not the easiest.`,
|
|
15
|
+
`3. A monitor is in place that detects when any sub-agent produces no output for 5 minutes.`,
|
|
16
|
+
`4. If you are blocked on an owner decision — or once you believe all tasks are complete — do not wait passively: the owner is not notified of passive waiting and will not reply unless you raise a new call-to-user, so raise one now (to get the decision, or to ask the owner to verify completion). If no owner input is needed yet, resume immediately and drive all remaining tasks to completion.`,
|
|
17
|
+
`Also, in your next output, report an estimate of how many minutes you expect to need to finish all remaining tasks.`,
|
|
18
|
+
].join('\n');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class DefaultSilentSessionMessageComposer implements SilentSessionMessageComposer {
|
|
22
|
+
composeMainStalledSection = (mainSilentSeconds: number): string => {
|
|
23
|
+
return composeMainStalledMessage(mainSilentSeconds);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
composeSubAgentSection = (subAgents: SubAgentActivity[]): string => {
|
|
27
|
+
const lines = subAgents.map(
|
|
28
|
+
(subAgent) =>
|
|
29
|
+
`- ${subAgent.label}: silent for ${formatMinutes(
|
|
30
|
+
subAgent.silentSeconds,
|
|
31
|
+
)}, running for ${formatMinutes(subAgent.runningSeconds)}`,
|
|
32
|
+
);
|
|
33
|
+
return [
|
|
34
|
+
'The following sub-processes have been silent or running for a long time:',
|
|
35
|
+
...lines,
|
|
36
|
+
'If a sub-process is stalled, take action (restart, hand off, or replace it). If it is legitimately waiting on an external dependency (continuous integration, an external API, or another process), let it continue.',
|
|
37
|
+
].join('\n');
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -22,10 +22,14 @@ const snapshot = (
|
|
|
22
22
|
const candidate = (
|
|
23
23
|
name: string,
|
|
24
24
|
snapshotValue: OauthTokenWindowSnapshot | null,
|
|
25
|
+
subscriptionDisabled = false,
|
|
26
|
+
unifiedRejected = false,
|
|
25
27
|
): OauthTokenCandidate => ({
|
|
26
28
|
name,
|
|
27
29
|
token: `fake-token-${name}`,
|
|
28
30
|
snapshot: snapshotValue,
|
|
31
|
+
subscriptionDisabled,
|
|
32
|
+
unifiedRejected,
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
const session = (name: string, sessionId: string): ClaudeLiveSession => ({
|
|
@@ -144,4 +148,40 @@ describe('LiveSessionOauthTokenSelectUseCase', () => {
|
|
|
144
148
|
const lonely = result.metrics.find((m) => m.name === 'lonely');
|
|
145
149
|
expect(lonely?.liveSessionCount).toBe(0);
|
|
146
150
|
});
|
|
151
|
+
|
|
152
|
+
it('excludes a subscription-disabled token even when it has zero live sessions', () => {
|
|
153
|
+
const result = useCase.run(
|
|
154
|
+
[
|
|
155
|
+
candidate('disabled', snapshot({}), true),
|
|
156
|
+
candidate('active', snapshot({}), false),
|
|
157
|
+
],
|
|
158
|
+
[session('active', 'session-a')],
|
|
159
|
+
NOW,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result.selected?.name).toBe('active');
|
|
163
|
+
const disabled = result.metrics.find((m) => m.name === 'disabled');
|
|
164
|
+
expect(disabled?.eligible).toBe(false);
|
|
165
|
+
expect(disabled?.liveSessionCount).toBe(0);
|
|
166
|
+
expect(disabled?.exclusionReason).toContain(
|
|
167
|
+
'organization has disabled Claude subscription access for Claude Code',
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('excludes a unified-rejected token even when it has zero live sessions', () => {
|
|
172
|
+
const result = useCase.run(
|
|
173
|
+
[
|
|
174
|
+
candidate('rejected', snapshot({}), false, true),
|
|
175
|
+
candidate('active', snapshot({}), false, false),
|
|
176
|
+
],
|
|
177
|
+
[session('active', 'session-a')],
|
|
178
|
+
NOW,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(result.selected?.name).toBe('active');
|
|
182
|
+
const rejected = result.metrics.find((m) => m.name === 'rejected');
|
|
183
|
+
expect(rejected?.eligible).toBe(false);
|
|
184
|
+
expect(rejected?.liveSessionCount).toBe(0);
|
|
185
|
+
expect(rejected?.exclusionReason).toContain('rejected');
|
|
186
|
+
});
|
|
147
187
|
});
|