@ai-devkit/agent-manager 0.9.0 → 0.11.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/dist/AgentManager.d.ts +21 -1
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +47 -0
- package/dist/AgentManager.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +66 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +14 -43
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +60 -275
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +14 -1
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +105 -6
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +9 -1
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +77 -6
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +36 -27
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +3 -12
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/utils/ClaudeSessionParser.d.ts +114 -0
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/utils/ClaudeSessionParser.js +377 -0
- package/dist/utils/ClaudeSessionParser.js.map +1 -0
- package/dist/utils/applescript.d.ts +6 -0
- package/dist/utils/applescript.d.ts.map +1 -0
- package/dist/utils/applescript.js +14 -0
- package/dist/utils/applescript.js.map +1 -0
- package/dist/utils/process.d.ts +3 -4
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +11 -15
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.d.ts +34 -5
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +90 -44
- package/dist/utils/session.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +66 -4
- package/src/__tests__/AgentManager.test.ts +134 -2
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +188 -32
- package/src/__tests__/adapters/CodexAdapter.test.ts +123 -3
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +133 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +195 -0
- package/src/__tests__/utils/process.test.ts +27 -27
- package/src/__tests__/utils/session.test.ts +79 -43
- package/src/adapters/AgentAdapter.ts +76 -0
- package/src/adapters/ClaudeCodeAdapter.ts +82 -356
- package/src/adapters/CodexAdapter.ts +126 -8
- package/src/adapters/GeminiCliAdapter.ts +102 -7
- package/src/index.ts +9 -1
- package/src/terminal/TerminalFocusManager.ts +35 -26
- package/src/terminal/TtyWriter.ts +1 -11
- package/src/utils/ClaudeSessionParser.ts +437 -0
- package/src/utils/applescript.ts +10 -0
- package/src/utils/process.ts +21 -24
- package/src/utils/session.ts +86 -45
|
@@ -760,6 +760,139 @@ describe('GeminiCliAdapter', () => {
|
|
|
760
760
|
expect(adapter.getConversation(sessionPath)).toEqual([]);
|
|
761
761
|
});
|
|
762
762
|
});
|
|
763
|
+
|
|
764
|
+
describe('listSessions', () => {
|
|
765
|
+
it('returns empty when ~/.gemini/tmp does not exist', async () => {
|
|
766
|
+
// tmpHome has no .gemini dir by default
|
|
767
|
+
const result = await adapter.listSessions();
|
|
768
|
+
expect(result).toEqual([]);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('walks every shortId/chats dir and returns sessions', async () => {
|
|
772
|
+
writeSession(tmpHome, 'aaa', 'session-1', {
|
|
773
|
+
sessionId: 's-1',
|
|
774
|
+
projectHash: hashProjectRoot('/repo-a'),
|
|
775
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
776
|
+
lastUpdated: '2025-01-01T00:01:00Z',
|
|
777
|
+
directories: ['/repo-a'],
|
|
778
|
+
messages: [
|
|
779
|
+
{ type: 'user', timestamp: '2025-01-01T00:00:00Z', content: [{ text: 'hello a' }] },
|
|
780
|
+
],
|
|
781
|
+
});
|
|
782
|
+
writeSession(tmpHome, 'bbb', 'session-2', {
|
|
783
|
+
sessionId: 's-2',
|
|
784
|
+
projectHash: hashProjectRoot('/repo-b'),
|
|
785
|
+
startTime: '2025-01-02T00:00:00Z',
|
|
786
|
+
lastUpdated: '2025-01-02T00:01:00Z',
|
|
787
|
+
directories: ['/repo-b'],
|
|
788
|
+
messages: [
|
|
789
|
+
{ type: 'user', timestamp: '2025-01-02T00:00:00Z', content: [{ text: 'hello b' }] },
|
|
790
|
+
],
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const result = await adapter.listSessions();
|
|
794
|
+
|
|
795
|
+
expect(result).toHaveLength(2);
|
|
796
|
+
const byId = Object.fromEntries(result.map((r) => [r.sessionId, r]));
|
|
797
|
+
expect(byId['s-1']).toMatchObject({
|
|
798
|
+
type: 'gemini_cli',
|
|
799
|
+
cwd: '/repo-a',
|
|
800
|
+
firstUserMessage: 'hello a',
|
|
801
|
+
});
|
|
802
|
+
expect(byId['s-2']).toMatchObject({
|
|
803
|
+
type: 'gemini_cli',
|
|
804
|
+
cwd: '/repo-b',
|
|
805
|
+
firstUserMessage: 'hello b',
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('applies strict-equality cwd filter against directories[0]', async () => {
|
|
810
|
+
writeSession(tmpHome, 'aaa', 'session-keep', {
|
|
811
|
+
sessionId: 'keep',
|
|
812
|
+
projectHash: hashProjectRoot('/repo'),
|
|
813
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
814
|
+
directories: ['/repo'],
|
|
815
|
+
messages: [{ type: 'user', timestamp: '2025-01-01T00:00:00Z', content: 'yes' }],
|
|
816
|
+
});
|
|
817
|
+
writeSession(tmpHome, 'bbb', 'session-drop', {
|
|
818
|
+
sessionId: 'drop',
|
|
819
|
+
projectHash: hashProjectRoot('/other'),
|
|
820
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
821
|
+
directories: ['/other'],
|
|
822
|
+
messages: [{ type: 'user', timestamp: '2025-01-01T00:00:00Z', content: 'no' }],
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const result = await adapter.listSessions({ cwd: '/repo' });
|
|
826
|
+
|
|
827
|
+
expect(result).toHaveLength(1);
|
|
828
|
+
expect(result[0].sessionId).toBe('keep');
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it('skips malformed JSON files', async () => {
|
|
832
|
+
const chatsDir = path.join(tmpHome, '.gemini', 'tmp', 'aaa', 'chats');
|
|
833
|
+
fs.mkdirSync(chatsDir, { recursive: true });
|
|
834
|
+
fs.writeFileSync(path.join(chatsDir, 'session-bad.json'), '{ not json');
|
|
835
|
+
writeSession(tmpHome, 'aaa', 'session-good', {
|
|
836
|
+
sessionId: 'good',
|
|
837
|
+
projectHash: hashProjectRoot('/repo'),
|
|
838
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
839
|
+
directories: ['/repo'],
|
|
840
|
+
messages: [{ type: 'user', timestamp: '2025-01-01T00:00:00Z', content: 'ok' }],
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const result = await adapter.listSessions();
|
|
844
|
+
expect(result).toHaveLength(1);
|
|
845
|
+
expect(result[0].sessionId).toBe('good');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('skips files missing sessionId', async () => {
|
|
849
|
+
writeSession(tmpHome, 'aaa', 'session-no-id', {
|
|
850
|
+
projectHash: hashProjectRoot('/repo'),
|
|
851
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
852
|
+
directories: ['/repo'],
|
|
853
|
+
messages: [],
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const result = await adapter.listSessions();
|
|
857
|
+
expect(result).toEqual([]);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('captures the first user-typed message as firstUserMessage', async () => {
|
|
861
|
+
writeSession(tmpHome, 'aaa', 'session-x', {
|
|
862
|
+
sessionId: 's',
|
|
863
|
+
projectHash: hashProjectRoot('/repo'),
|
|
864
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
865
|
+
directories: ['/repo'],
|
|
866
|
+
messages: [
|
|
867
|
+
{ type: 'gemini', timestamp: '2025-01-01T00:00:00Z', content: 'preamble' },
|
|
868
|
+
{ type: 'user', timestamp: '2025-01-01T00:00:01Z', content: [{ text: 'first user' }] },
|
|
869
|
+
{ type: 'user', timestamp: '2025-01-01T00:00:02Z', content: 'second user' },
|
|
870
|
+
],
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
const result = await adapter.listSessions({ cwd: '/repo' });
|
|
874
|
+
|
|
875
|
+
expect(result).toHaveLength(1);
|
|
876
|
+
expect(result[0].firstUserMessage).toBe('first user');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('returns empty firstUserMessage when no user message exists', async () => {
|
|
880
|
+
writeSession(tmpHome, 'aaa', 'session-x', {
|
|
881
|
+
sessionId: 's',
|
|
882
|
+
projectHash: hashProjectRoot('/repo'),
|
|
883
|
+
startTime: '2025-01-01T00:00:00Z',
|
|
884
|
+
directories: ['/repo'],
|
|
885
|
+
messages: [
|
|
886
|
+
{ type: 'gemini', timestamp: '2025-01-01T00:00:00Z', content: 'agent only' },
|
|
887
|
+
],
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const result = await adapter.listSessions({ cwd: '/repo' });
|
|
891
|
+
|
|
892
|
+
expect(result).toHaveLength(1);
|
|
893
|
+
expect(result[0].firstUserMessage).toBe('');
|
|
894
|
+
});
|
|
895
|
+
});
|
|
763
896
|
});
|
|
764
897
|
|
|
765
898
|
/**
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for utils/ClaudeSessionParser.ts — focused on stripping
|
|
3
|
+
* harness-injected XML tags from conversation content.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { ClaudeSessionParser } from '../../utils/ClaudeSessionParser';
|
|
11
|
+
|
|
12
|
+
interface JsonlEntry {
|
|
13
|
+
type: 'user' | 'assistant' | 'system';
|
|
14
|
+
message: { content: string | Array<{ type: string; text?: string; content?: string; name?: string; input?: unknown; is_error?: boolean }> };
|
|
15
|
+
timestamp?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeSession(entries: JsonlEntry[]): string {
|
|
19
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'parser-test-'));
|
|
20
|
+
const filePath = path.join(dir, 'session.jsonl');
|
|
21
|
+
fs.writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join('\n'));
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('ClaudeSessionParser.getConversation — harness tag stripping', () => {
|
|
26
|
+
let parser: ClaudeSessionParser;
|
|
27
|
+
const tempFiles: string[] = [];
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
parser = new ClaudeSessionParser();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
for (const f of tempFiles) {
|
|
35
|
+
try {
|
|
36
|
+
fs.rmSync(path.dirname(f), { recursive: true, force: true });
|
|
37
|
+
} catch { /* best effort */ }
|
|
38
|
+
}
|
|
39
|
+
tempFiles.length = 0;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function makeSession(entries: JsonlEntry[]): string {
|
|
43
|
+
const f = writeSession(entries);
|
|
44
|
+
tempFiles.push(f);
|
|
45
|
+
return f;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it('strips <system-reminder> blocks from text content', () => {
|
|
49
|
+
const file = makeSession([
|
|
50
|
+
{
|
|
51
|
+
type: 'assistant',
|
|
52
|
+
message: {
|
|
53
|
+
content: [{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: 'Real response here.\n<system-reminder>\nDo not mention this reminder.\n</system-reminder>\nMore response.',
|
|
56
|
+
}],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const conv = parser.getConversation(file);
|
|
62
|
+
expect(conv).toHaveLength(1);
|
|
63
|
+
expect(conv[0].content).toBe('Real response here.\n\nMore response.');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('strips <local-command-stdout> blocks', () => {
|
|
67
|
+
const file = makeSession([
|
|
68
|
+
{
|
|
69
|
+
type: 'system',
|
|
70
|
+
message: { content: 'before <local-command-stdout>\nRunning...\nDone\n</local-command-stdout> after' },
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const conv = parser.getConversation(file);
|
|
75
|
+
expect(conv[0].content).toBe('before after');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('strips <user-prompt-submit-hook> blocks', () => {
|
|
79
|
+
const file = makeSession([
|
|
80
|
+
{
|
|
81
|
+
type: 'user',
|
|
82
|
+
message: { content: '<user-prompt-submit-hook>hook output</user-prompt-submit-hook>\nactual question' },
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const conv = parser.getConversation(file);
|
|
87
|
+
expect(conv[0].content).toBe('actual question');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('strips bash and command stdout/stderr blocks', () => {
|
|
91
|
+
const file = makeSession([
|
|
92
|
+
{
|
|
93
|
+
type: 'assistant',
|
|
94
|
+
message: {
|
|
95
|
+
content: [{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: 'Output:\n<bash-input>ls</bash-input>\n<bash-stdout>file.txt</bash-stdout>\n<bash-stderr></bash-stderr>\n<command-stdout>x</command-stdout>\n<command-stderr>y</command-stderr>\nEnd.',
|
|
98
|
+
}],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const conv = parser.getConversation(file);
|
|
104
|
+
expect(conv[0].content).toBe('Output:\n\n\n\n\n\nEnd.'.replace(/\n{3,}/g, '\n\n'));
|
|
105
|
+
expect(conv[0].content).not.toMatch(/<bash-/);
|
|
106
|
+
expect(conv[0].content).not.toMatch(/<command-stdout>|<command-stderr>/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('collapses <command-name>/<command-args> into "/name args" shorthand', () => {
|
|
110
|
+
const file = makeSession([
|
|
111
|
+
{
|
|
112
|
+
type: 'user',
|
|
113
|
+
message: {
|
|
114
|
+
content: '<command-message>debug</command-message>\n<command-name>/debug</command-name>\n<command-args>fix the bug</command-args>',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const conv = parser.getConversation(file);
|
|
120
|
+
expect(conv[0].content).toBe('/debug fix the bug');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('handles <command-name> without <command-args>', () => {
|
|
124
|
+
const file = makeSession([
|
|
125
|
+
{
|
|
126
|
+
type: 'user',
|
|
127
|
+
message: {
|
|
128
|
+
content: '<command-message>clear</command-message>\n<command-name>/clear</command-name>',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
const conv = parser.getConversation(file);
|
|
134
|
+
expect(conv[0].content).toBe('/clear');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('handles multiple harness tags mixed together', () => {
|
|
138
|
+
const file = makeSession([
|
|
139
|
+
{
|
|
140
|
+
type: 'user',
|
|
141
|
+
message: {
|
|
142
|
+
content: [{
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: '<system-reminder>ignore me</system-reminder>\n<command-message>build</command-message>\n<command-name>/build</command-name>\n<command-args>--watch</command-args>\n<local-command-stdout>build output here</local-command-stdout>',
|
|
145
|
+
}],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const conv = parser.getConversation(file);
|
|
151
|
+
expect(conv[0].content).toBe('/build --watch');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('leaves text without harness tags unchanged', () => {
|
|
155
|
+
const file = makeSession([
|
|
156
|
+
{
|
|
157
|
+
type: 'assistant',
|
|
158
|
+
message: { content: [{ type: 'text', text: 'Hello, world!' }] },
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const conv = parser.getConversation(file);
|
|
163
|
+
expect(conv[0].content).toBe('Hello, world!');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('drops a message that becomes empty after stripping', () => {
|
|
167
|
+
const file = makeSession([
|
|
168
|
+
{
|
|
169
|
+
type: 'system',
|
|
170
|
+
message: { content: '<system-reminder>only this</system-reminder>' },
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'assistant',
|
|
174
|
+
message: { content: [{ type: 'text', text: 'kept' }] },
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const conv = parser.getConversation(file);
|
|
179
|
+
expect(conv).toHaveLength(1);
|
|
180
|
+
expect(conv[0].content).toBe('kept');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('strips tags spanning multiple lines', () => {
|
|
184
|
+
const multilineReminder = '<system-reminder>\nLine 1\nLine 2\nLine 3\n</system-reminder>';
|
|
185
|
+
const file = makeSession([
|
|
186
|
+
{
|
|
187
|
+
type: 'assistant',
|
|
188
|
+
message: { content: [{ type: 'text', text: `before\n${multilineReminder}\nafter` }] },
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const conv = parser.getConversation(file);
|
|
193
|
+
expect(conv[0].content).toBe('before\n\nafter');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
7
|
import {
|
|
8
8
|
listAgentProcesses,
|
|
9
9
|
batchGetProcessCwds,
|
|
@@ -12,18 +12,18 @@ import {
|
|
|
12
12
|
} from '../../utils/process';
|
|
13
13
|
|
|
14
14
|
jest.mock('child_process', () => ({
|
|
15
|
-
|
|
15
|
+
execFileSync: jest.fn(),
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const mockedExecFileSync = execFileSync as jest.MockedFunction<typeof execFileSync>;
|
|
19
19
|
|
|
20
20
|
describe('listAgentProcesses', () => {
|
|
21
21
|
beforeEach(() => {
|
|
22
|
-
|
|
22
|
+
mockedExecFileSync.mockReset();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('should parse ps aux | grep output and post-filter by executable name', () => {
|
|
26
|
-
|
|
26
|
+
mockedExecFileSync.mockReturnValue(
|
|
27
27
|
'user 78070 1.0 0.5 485636016 245952 s018 S+ 11:18PM 1:55.14 claude\n' +
|
|
28
28
|
'user 55106 0.1 0.4 485620368 72496 s015 S+ 9Mar26 8:06.36 claude\n',
|
|
29
29
|
);
|
|
@@ -38,7 +38,7 @@ describe('listAgentProcesses', () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('should filter out non-matching executables', () => {
|
|
41
|
-
|
|
41
|
+
mockedExecFileSync.mockReturnValue(
|
|
42
42
|
'user 100 0.0 0.0 0 0 s001 S 1:00PM 0:00 claude\n' +
|
|
43
43
|
'user 200 0.0 0.0 0 0 s002 S 1:00PM 0:00 claude-helper --pid 100\n' +
|
|
44
44
|
'user 300 0.0 0.0 0 0 s003 S 1:00PM 0:00 /usr/bin/claude\n',
|
|
@@ -50,46 +50,46 @@ describe('listAgentProcesses', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('should return empty array on command failure', () => {
|
|
53
|
-
|
|
53
|
+
mockedExecFileSync.mockImplementation(() => { throw new Error('fail'); });
|
|
54
54
|
expect(listAgentProcesses('claude')).toEqual([]);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('should handle empty output', () => {
|
|
58
|
-
|
|
58
|
+
mockedExecFileSync.mockReturnValue('');
|
|
59
59
|
expect(listAgentProcesses('claude')).toEqual([]);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('should reject empty pattern', () => {
|
|
63
63
|
expect(listAgentProcesses('')).toEqual([]);
|
|
64
|
-
expect(
|
|
64
|
+
expect(mockedExecFileSync).not.toHaveBeenCalled();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it('should reject patterns with shell injection characters', () => {
|
|
68
68
|
expect(listAgentProcesses('claude; rm -rf /')).toEqual([]);
|
|
69
69
|
expect(listAgentProcesses("claude' || true")).toEqual([]);
|
|
70
70
|
expect(listAgentProcesses('$(whoami)')).toEqual([]);
|
|
71
|
-
expect(
|
|
71
|
+
expect(mockedExecFileSync).not.toHaveBeenCalled();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
it('should accept valid patterns with dashes and underscores', () => {
|
|
75
|
-
|
|
75
|
+
mockedExecFileSync.mockReturnValue('');
|
|
76
76
|
listAgentProcesses('claude-code');
|
|
77
|
-
expect(
|
|
77
|
+
expect(mockedExecFileSync).toHaveBeenCalled();
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
mockedExecFileSync.mockReset();
|
|
80
|
+
mockedExecFileSync.mockReturnValue('');
|
|
81
81
|
listAgentProcesses('my_agent');
|
|
82
|
-
expect(
|
|
82
|
+
expect(mockedExecFileSync).toHaveBeenCalled();
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
describe('batchGetProcessCwds', () => {
|
|
87
87
|
beforeEach(() => {
|
|
88
|
-
|
|
88
|
+
mockedExecFileSync.mockReset();
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('should parse batched lsof output', () => {
|
|
92
|
-
|
|
92
|
+
mockedExecFileSync.mockReturnValue(
|
|
93
93
|
'p78070\nn/Users/user/ai-devkit\np55106\nn/Users/user/other-project\n',
|
|
94
94
|
);
|
|
95
95
|
|
|
@@ -104,7 +104,7 @@ describe('batchGetProcessCwds', () => {
|
|
|
104
104
|
|
|
105
105
|
it('should return partial results when lsof succeeds for some PIDs', () => {
|
|
106
106
|
// lsof might not return entries for dead processes
|
|
107
|
-
|
|
107
|
+
mockedExecFileSync.mockReturnValue(
|
|
108
108
|
'p78070\nn/Users/user/ai-devkit\n',
|
|
109
109
|
);
|
|
110
110
|
|
|
@@ -114,7 +114,7 @@ describe('batchGetProcessCwds', () => {
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
it('should return empty map on total failure', () => {
|
|
117
|
-
|
|
117
|
+
mockedExecFileSync.mockImplementation(() => { throw new Error('fail'); });
|
|
118
118
|
const cwds = batchGetProcessCwds([78070]);
|
|
119
119
|
// Falls through to pwdx fallback which also fails
|
|
120
120
|
expect(cwds.size).toBe(0);
|
|
@@ -123,11 +123,11 @@ describe('batchGetProcessCwds', () => {
|
|
|
123
123
|
|
|
124
124
|
describe('batchGetProcessStartTimes', () => {
|
|
125
125
|
beforeEach(() => {
|
|
126
|
-
|
|
126
|
+
mockedExecFileSync.mockReset();
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
it('should parse ps lstart output', () => {
|
|
130
|
-
|
|
130
|
+
mockedExecFileSync.mockReturnValue(
|
|
131
131
|
' 78070 Wed Mar 18 23:18:01 2026\n' +
|
|
132
132
|
' 55106 Mon Mar 9 21:41:42 2026\n',
|
|
133
133
|
);
|
|
@@ -143,7 +143,7 @@ describe('batchGetProcessStartTimes', () => {
|
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
it('should skip lines with unparseable dates', () => {
|
|
146
|
-
|
|
146
|
+
mockedExecFileSync.mockReturnValue(
|
|
147
147
|
' 78070 Wed Mar 18 23:18:01 2026\n' +
|
|
148
148
|
' 99999 INVALID_DATE\n',
|
|
149
149
|
);
|
|
@@ -154,20 +154,20 @@ describe('batchGetProcessStartTimes', () => {
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
it('should return empty map on failure', () => {
|
|
157
|
-
|
|
157
|
+
mockedExecFileSync.mockImplementation(() => { throw new Error('fail'); });
|
|
158
158
|
expect(batchGetProcessStartTimes([78070])).toEqual(new Map());
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
describe('enrichProcesses', () => {
|
|
163
163
|
beforeEach(() => {
|
|
164
|
-
|
|
164
|
+
mockedExecFileSync.mockReset();
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
it('should populate cwd and startTime on processes', () => {
|
|
168
168
|
// First call: batchGetProcessCwds (lsof)
|
|
169
169
|
// Second call: batchGetProcessStartTimes (ps lstart)
|
|
170
|
-
|
|
170
|
+
mockedExecFileSync
|
|
171
171
|
.mockReturnValueOnce('p100\nn/projects/app\n')
|
|
172
172
|
.mockReturnValueOnce(' 100 Wed Mar 18 23:18:01 2026\n');
|
|
173
173
|
|
|
@@ -182,12 +182,12 @@ describe('enrichProcesses', () => {
|
|
|
182
182
|
|
|
183
183
|
it('should return empty array for empty input', () => {
|
|
184
184
|
expect(enrichProcesses([])).toEqual([]);
|
|
185
|
-
expect(
|
|
185
|
+
expect(mockedExecFileSync).not.toHaveBeenCalled();
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
it('should handle partial failures', () => {
|
|
189
189
|
// lsof succeeds, ps lstart fails
|
|
190
|
-
|
|
190
|
+
mockedExecFileSync
|
|
191
191
|
.mockReturnValueOnce('p100\nn/projects/app\n')
|
|
192
192
|
.mockImplementationOnce(() => { throw new Error('fail'); });
|
|
193
193
|
|