@iservu-inc/adf-cli 0.17.5 → 0.18.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/.project/chats/current/SESSION-STATUS.md +29 -27
- package/.project/docs/ROADMAP.md +74 -64
- package/CHANGELOG.md +78 -0
- package/README.md +63 -27
- package/bin/adf.js +54 -0
- package/lib/analysis/dynamic-pipeline.js +26 -0
- package/lib/analysis/knowledge-graph.js +66 -0
- package/lib/commands/deploy.js +35 -0
- package/lib/commands/harness.js +345 -0
- package/lib/commands/init.js +133 -9
- package/lib/frameworks/interviewer.js +130 -0
- package/lib/frameworks/progress-tracker.js +30 -1
- package/lib/frameworks/session-manager.js +76 -0
- package/lib/harness/context-window-manager.js +255 -0
- package/lib/harness/event-logger.js +115 -0
- package/lib/harness/feature-manifest.js +175 -0
- package/lib/harness/headless-adapter.js +184 -0
- package/lib/harness/milestone-tracker.js +183 -0
- package/lib/harness/protocol.js +503 -0
- package/lib/harness/provider-bridge.js +226 -0
- package/lib/harness/run-manager.js +267 -0
- package/lib/utils/project-detector.js +5 -1
- package/lib/utils/tool-detector.js +167 -0
- package/lib/utils/tool-feature-registry.js +82 -13
- package/lib/utils/tool-recommender.js +325 -0
- package/package.json +1 -1
- package/tests/harness-backward-compat.test.js +251 -0
- package/tests/harness-context-window.test.js +310 -0
- package/tests/harness-event-logger.test.js +148 -0
- package/tests/harness-feature-manifest.test.js +124 -0
- package/tests/harness-headless-adapter.test.js +196 -0
- package/tests/harness-integration.test.js +207 -0
- package/tests/harness-milestone-tracker.test.js +158 -0
- package/tests/harness-protocol.test.js +341 -0
- package/tests/harness-provider-bridge.test.js +180 -0
- package/tests/harness-provider-switch.test.js +204 -0
- package/tests/harness-run-manager.test.js +131 -0
- package/tests/tool-detector.test.js +152 -0
- package/tests/tool-recommender.test.js +218 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const RunManager = require('../lib/harness/run-manager');
|
|
4
|
+
const ContextWindowManager = require('../lib/harness/context-window-manager');
|
|
5
|
+
const ProviderBridge = require('../lib/harness/provider-bridge');
|
|
6
|
+
const KnowledgeGraph = require('../lib/analysis/knowledge-graph');
|
|
7
|
+
|
|
8
|
+
describe('Harness Provider Switch', () => {
|
|
9
|
+
const tempDir = path.join(__dirname, 'temp-provider-switch');
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
await fs.ensureDir(tempDir);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.remove(tempDir);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should transfer handoff between Anthropic and OpenAI', async () => {
|
|
20
|
+
// Phase 1: Start with Anthropic
|
|
21
|
+
const manager = new RunManager(tempDir);
|
|
22
|
+
const run = await manager.createRun({
|
|
23
|
+
workflow: 'balanced',
|
|
24
|
+
provider: { id: 'anthropic', model: 'claude-opus-4-6', contextWindowSize: 200000 }
|
|
25
|
+
});
|
|
26
|
+
await manager.startRun(run.id);
|
|
27
|
+
|
|
28
|
+
const runDir = manager.getRunDir(run.id);
|
|
29
|
+
const cwManager = new ContextWindowManager(runDir, run.id);
|
|
30
|
+
|
|
31
|
+
// Open window 1 (Anthropic)
|
|
32
|
+
await cwManager.openWindow({ tokenLimit: 200000 });
|
|
33
|
+
|
|
34
|
+
// Build knowledge
|
|
35
|
+
const kg = new KnowledgeGraph(path.join(tempDir, 'session'));
|
|
36
|
+
await fs.ensureDir(path.join(tempDir, 'session'));
|
|
37
|
+
kg.add([
|
|
38
|
+
{ type: 'tech_stack', content: 'React with TypeScript', confidence: 95, source: 'q1' },
|
|
39
|
+
{ type: 'architecture', content: 'REST API with Express', confidence: 88, source: 'q2' }
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// Generate handoff with Anthropic context
|
|
43
|
+
const handoff = await cwManager.generateHandoffPackage({
|
|
44
|
+
progressSummary: '5 of 15 questions answered with Claude Opus 4.6',
|
|
45
|
+
completedWork: ['Block 1', 'Block 2'],
|
|
46
|
+
nextSteps: ['Block 3'],
|
|
47
|
+
knowledgeGraph: kg.toHandoffFormat(),
|
|
48
|
+
criticalAnswers: [
|
|
49
|
+
{ questionId: 'q1', summary: 'React + TS', quality: 95 },
|
|
50
|
+
{ questionId: 'q2', summary: 'Express REST', quality: 88 }
|
|
51
|
+
],
|
|
52
|
+
position: {
|
|
53
|
+
currentBlock: 3,
|
|
54
|
+
totalBlocks: 5,
|
|
55
|
+
answeredIds: ['q1', 'q2', 'q3', 'q4', 'q5'],
|
|
56
|
+
remainingIds: ['q6', 'q7', 'q8', 'q9', 'q10']
|
|
57
|
+
},
|
|
58
|
+
activeRules: [{ id: 'rule_1', type: 'skip', questionId: 'q_skip' }],
|
|
59
|
+
sourceProviderId: 'anthropic'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await cwManager.closeWindow('completed');
|
|
63
|
+
await manager.pauseRun(run.id);
|
|
64
|
+
|
|
65
|
+
// Phase 2: Resume with OpenAI
|
|
66
|
+
await manager.resumeRun(run.id);
|
|
67
|
+
|
|
68
|
+
// Update run provider
|
|
69
|
+
const updatedRun = await manager.loadRun(run.id);
|
|
70
|
+
updatedRun.provider = { id: 'openai', model: 'gpt-5.2', contextWindowSize: 400000 };
|
|
71
|
+
await manager.saveRun(updatedRun);
|
|
72
|
+
|
|
73
|
+
// Open window 2 (OpenAI context)
|
|
74
|
+
const window2 = await cwManager.openWindow({ tokenLimit: 400000 });
|
|
75
|
+
expect(window2.sequence).toBe(2);
|
|
76
|
+
|
|
77
|
+
// Consume handoff
|
|
78
|
+
const consumed = await cwManager.consumeHandoffPackage('latest');
|
|
79
|
+
expect(consumed).not.toBeNull();
|
|
80
|
+
|
|
81
|
+
// Verify all data transferred correctly
|
|
82
|
+
expect(consumed.progressSummary).toContain('Claude Opus 4.6');
|
|
83
|
+
expect(consumed.sourceProviderId).toBe('anthropic');
|
|
84
|
+
expect(consumed.position.answeredIds.length).toBe(5);
|
|
85
|
+
expect(consumed.position.remainingIds.length).toBe(5);
|
|
86
|
+
expect(consumed.knowledgeGraph.highConfidenceItems.length).toBe(2);
|
|
87
|
+
expect(consumed.activeRules.length).toBe(1);
|
|
88
|
+
expect(consumed.criticalAnswers.length).toBe(2);
|
|
89
|
+
|
|
90
|
+
// Restore knowledge graph
|
|
91
|
+
const kg2 = new KnowledgeGraph(path.join(tempDir, 'session2'));
|
|
92
|
+
await fs.ensureDir(path.join(tempDir, 'session2'));
|
|
93
|
+
kg2.fromHandoffFormat(consumed.knowledgeGraph);
|
|
94
|
+
expect(kg2.has('tech_stack', 90)).toBe(true);
|
|
95
|
+
expect(kg2.has('architecture', 85)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should transfer handoff between OpenAI and Google Gemini', async () => {
|
|
99
|
+
const manager = new RunManager(tempDir);
|
|
100
|
+
const run = await manager.createRun({
|
|
101
|
+
workflow: 'comprehensive',
|
|
102
|
+
provider: { id: 'openai', model: 'gpt-5.2', contextWindowSize: 400000 }
|
|
103
|
+
});
|
|
104
|
+
await manager.startRun(run.id);
|
|
105
|
+
|
|
106
|
+
const runDir = manager.getRunDir(run.id);
|
|
107
|
+
const cwManager = new ContextWindowManager(runDir, run.id);
|
|
108
|
+
await cwManager.openWindow();
|
|
109
|
+
|
|
110
|
+
// Generate handoff from OpenAI context
|
|
111
|
+
const handoff = await cwManager.generateHandoffPackage({
|
|
112
|
+
progressSummary: 'Phase 1 done with GPT-5.2',
|
|
113
|
+
position: {
|
|
114
|
+
currentBlock: 2,
|
|
115
|
+
totalBlocks: 8,
|
|
116
|
+
answeredIds: ['q1', 'q2', 'q3'],
|
|
117
|
+
remainingIds: ['q4', 'q5', 'q6', 'q7', 'q8']
|
|
118
|
+
},
|
|
119
|
+
knowledgeGraph: {
|
|
120
|
+
highConfidenceItems: [
|
|
121
|
+
{ type: 'tech_stack', content: 'Python FastAPI', confidence: 92, sources: ['q1'] }
|
|
122
|
+
],
|
|
123
|
+
totalItems: 1,
|
|
124
|
+
typesSeen: ['tech_stack']
|
|
125
|
+
},
|
|
126
|
+
sourceProviderId: 'openai'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await cwManager.closeWindow();
|
|
130
|
+
await manager.pauseRun(run.id);
|
|
131
|
+
|
|
132
|
+
// Resume with Google Gemini
|
|
133
|
+
await manager.resumeRun(run.id);
|
|
134
|
+
const updatedRun = await manager.loadRun(run.id);
|
|
135
|
+
updatedRun.provider = { id: 'google', model: 'gemini-3.1-pro-preview', contextWindowSize: 1000000 };
|
|
136
|
+
await manager.saveRun(updatedRun);
|
|
137
|
+
|
|
138
|
+
await cwManager.openWindow({ tokenLimit: 1000000 });
|
|
139
|
+
const consumed = await cwManager.consumeHandoffPackage('latest');
|
|
140
|
+
|
|
141
|
+
expect(consumed.sourceProviderId).toBe('openai');
|
|
142
|
+
expect(consumed.position.answeredIds.length).toBe(3);
|
|
143
|
+
expect(consumed.knowledgeGraph.highConfidenceItems[0].content).toBe('Python FastAPI');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('provider bridge capabilities should be independent of handoff', () => {
|
|
147
|
+
const mockClient = {
|
|
148
|
+
sendMessage: jest.fn().mockResolvedValue({ text: 'ok', usage: { inputTokens: 10, outputTokens: 5 } })
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Start with Anthropic bridge
|
|
152
|
+
const bridge1 = new ProviderBridge(mockClient, 'anthropic', 'claude-opus-4-6');
|
|
153
|
+
expect(bridge1.getContextWindowSize(false)).toBe(200000);
|
|
154
|
+
expect(bridge1.getContextWindowSize(true)).toBe(1000000);
|
|
155
|
+
expect(bridge1.supportsCompaction()).toBe(true);
|
|
156
|
+
|
|
157
|
+
// Switch to OpenAI bridge (same client, different capabilities)
|
|
158
|
+
const bridge2 = new ProviderBridge(mockClient, 'openai', 'gpt-5.2');
|
|
159
|
+
expect(bridge2.getContextWindowSize(false)).toBe(400000);
|
|
160
|
+
expect(bridge2.supportsCompaction()).toBe(true);
|
|
161
|
+
|
|
162
|
+
// Switch to Google bridge (largest context)
|
|
163
|
+
const bridge3 = new ProviderBridge(mockClient, 'google', 'gemini-3.1-pro-preview');
|
|
164
|
+
expect(bridge3.getContextWindowSize(false)).toBe(1000000);
|
|
165
|
+
expect(bridge3.supportsInteractionsAPI()).toBe(true);
|
|
166
|
+
expect(bridge3.supportsThoughtSignatures()).toBe(true);
|
|
167
|
+
|
|
168
|
+
// Provider info for handoff is correct
|
|
169
|
+
expect(bridge1.getProviderInfo().id).toBe('anthropic');
|
|
170
|
+
expect(bridge2.getProviderInfo().id).toBe('openai');
|
|
171
|
+
expect(bridge3.getProviderInfo().id).toBe('google');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('handoff package should be provider-agnostic format', async () => {
|
|
175
|
+
const manager = new RunManager(tempDir);
|
|
176
|
+
const run = await manager.createRun({ workflow: 'rapid' });
|
|
177
|
+
await manager.startRun(run.id);
|
|
178
|
+
|
|
179
|
+
const runDir = manager.getRunDir(run.id);
|
|
180
|
+
const cwManager = new ContextWindowManager(runDir, run.id);
|
|
181
|
+
await cwManager.openWindow();
|
|
182
|
+
|
|
183
|
+
const handoff = await cwManager.generateHandoffPackage({
|
|
184
|
+
progressSummary: 'Test',
|
|
185
|
+
position: { currentBlock: 1, totalBlocks: 3, answeredIds: ['q1'], remainingIds: ['q2'] },
|
|
186
|
+
sourceProviderId: 'anthropic'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Handoff should contain no provider-specific API keys, tokens, or formats
|
|
190
|
+
const json = handoff.toJSON();
|
|
191
|
+
const jsonStr = JSON.stringify(json);
|
|
192
|
+
|
|
193
|
+
// Should not contain API key patterns
|
|
194
|
+
expect(jsonStr).not.toContain('sk-ant-');
|
|
195
|
+
expect(jsonStr).not.toContain('sk-');
|
|
196
|
+
expect(jsonStr).not.toContain('API_KEY');
|
|
197
|
+
|
|
198
|
+
// Should contain only portable data
|
|
199
|
+
expect(json.progressSummary).toBeDefined();
|
|
200
|
+
expect(json.position).toBeDefined();
|
|
201
|
+
expect(json.knowledgeGraph).toBeDefined();
|
|
202
|
+
expect(json.sourceProviderId).toBe('anthropic');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const RunManager = require('../lib/harness/run-manager');
|
|
4
|
+
|
|
5
|
+
describe('RunManager', () => {
|
|
6
|
+
const tempDir = path.join(__dirname, 'temp-run-manager-test');
|
|
7
|
+
let manager;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await fs.ensureDir(tempDir);
|
|
11
|
+
manager = new RunManager(tempDir);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.remove(tempDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should create a new run', async () => {
|
|
19
|
+
const run = await manager.createRun({ workflow: 'rapid' });
|
|
20
|
+
expect(run.id).toMatch(/^run_/);
|
|
21
|
+
expect(run.status).toBe('initializing');
|
|
22
|
+
expect(run.workflow).toBe('rapid');
|
|
23
|
+
|
|
24
|
+
// Should have created files
|
|
25
|
+
const runDir = manager.getRunDir(run.id);
|
|
26
|
+
expect(await fs.pathExists(path.join(runDir, 'harness-run.json'))).toBe(true);
|
|
27
|
+
expect(await fs.pathExists(path.join(runDir, 'context-windows'))).toBe(true);
|
|
28
|
+
expect(await fs.pathExists(path.join(runDir, 'milestones'))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should set current-run pointer on create', async () => {
|
|
32
|
+
const run = await manager.createRun();
|
|
33
|
+
const currentId = await manager.getCurrentRunId();
|
|
34
|
+
expect(currentId).toBe(run.id);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should load current run', async () => {
|
|
38
|
+
const created = await manager.createRun({ workflow: 'comprehensive' });
|
|
39
|
+
const loaded = await manager.getCurrentRun();
|
|
40
|
+
expect(loaded).not.toBeNull();
|
|
41
|
+
expect(loaded.id).toBe(created.id);
|
|
42
|
+
expect(loaded.workflow).toBe('comprehensive');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should return null when no current run', async () => {
|
|
46
|
+
const run = await manager.getCurrentRun();
|
|
47
|
+
expect(run).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should start a run', async () => {
|
|
51
|
+
const created = await manager.createRun();
|
|
52
|
+
const started = await manager.startRun(created.id);
|
|
53
|
+
expect(started.status).toBe('running');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should pause a run', async () => {
|
|
57
|
+
const created = await manager.createRun();
|
|
58
|
+
await manager.startRun(created.id);
|
|
59
|
+
const paused = await manager.pauseRun(created.id);
|
|
60
|
+
expect(paused.status).toBe('paused');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should resume a paused run', async () => {
|
|
64
|
+
const created = await manager.createRun();
|
|
65
|
+
await manager.startRun(created.id);
|
|
66
|
+
await manager.pauseRun(created.id);
|
|
67
|
+
const resumed = await manager.resumeRun(created.id);
|
|
68
|
+
expect(resumed.status).toBe('running');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should fail to resume non-paused run', async () => {
|
|
72
|
+
const created = await manager.createRun();
|
|
73
|
+
await manager.startRun(created.id);
|
|
74
|
+
await expect(manager.resumeRun(created.id)).rejects.toThrow('not paused');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should complete a run', async () => {
|
|
78
|
+
const created = await manager.createRun();
|
|
79
|
+
await manager.startRun(created.id);
|
|
80
|
+
const completed = await manager.completeRun(created.id);
|
|
81
|
+
expect(completed.status).toBe('completed');
|
|
82
|
+
|
|
83
|
+
// Should clear current-run pointer
|
|
84
|
+
const currentId = await manager.getCurrentRunId();
|
|
85
|
+
expect(currentId).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should fail a run', async () => {
|
|
89
|
+
const created = await manager.createRun();
|
|
90
|
+
await manager.startRun(created.id);
|
|
91
|
+
const failed = await manager.failRun(created.id, 'Something went wrong');
|
|
92
|
+
expect(failed.status).toBe('failed');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should list all runs', async () => {
|
|
96
|
+
await manager.createRun({ workflow: 'rapid' });
|
|
97
|
+
await manager.createRun({ workflow: 'balanced' });
|
|
98
|
+
|
|
99
|
+
const runs = await manager.listRuns();
|
|
100
|
+
expect(runs.length).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should return empty list when no runs', async () => {
|
|
104
|
+
const runs = await manager.listRuns();
|
|
105
|
+
expect(runs).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('should throw on non-existent run', async () => {
|
|
109
|
+
await expect(manager.startRun('run_nonexistent')).rejects.toThrow('Run not found');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should create run with provider info', async () => {
|
|
113
|
+
const run = await manager.createRun({
|
|
114
|
+
provider: { id: 'anthropic', model: 'claude-opus-4-6', contextWindowSize: 200000 }
|
|
115
|
+
});
|
|
116
|
+
expect(run.provider.id).toBe('anthropic');
|
|
117
|
+
expect(run.provider.model).toBe('claude-opus-4-6');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should create run with session linkage', async () => {
|
|
121
|
+
const run = await manager.createRun({ sessionId: '2026-01-01T00-00-00_balanced' });
|
|
122
|
+
expect(run.sessionId).toBe('2026-01-01T00-00-00_balanced');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should get event logger for run', async () => {
|
|
126
|
+
const run = await manager.createRun();
|
|
127
|
+
const logger = manager.getEventLogger(run.id);
|
|
128
|
+
expect(logger).toBeDefined();
|
|
129
|
+
expect(logger.filePath).toContain(run.id);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const ToolDetector = require('../lib/utils/tool-detector');
|
|
5
|
+
|
|
6
|
+
describe('ToolDetector', () => {
|
|
7
|
+
describe('expandPath', () => {
|
|
8
|
+
test('should expand ~ to home directory', () => {
|
|
9
|
+
const result = ToolDetector.expandPath('~/.cursor/');
|
|
10
|
+
expect(result).toBe(path.join(os.homedir(), '.cursor/'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('should expand %APPDATA% style env vars', () => {
|
|
14
|
+
process.env.TEST_VAR_XYZ = '/test/path';
|
|
15
|
+
const result = ToolDetector.expandPath('%TEST_VAR_XYZ%/Cursor/');
|
|
16
|
+
expect(result).toBe('/test/path/Cursor/');
|
|
17
|
+
delete process.env.TEST_VAR_XYZ;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should return path unchanged when no expansion needed', () => {
|
|
21
|
+
const result = ToolDetector.expandPath('/usr/local/bin/cursor');
|
|
22
|
+
expect(result).toBe('/usr/local/bin/cursor');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should replace unknown env vars with empty string', () => {
|
|
26
|
+
const result = ToolDetector.expandPath('%NONEXISTENT_VAR_12345%/path');
|
|
27
|
+
expect(result).toBe('/path');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('execQuiet', () => {
|
|
32
|
+
test('should resolve for a valid command', async () => {
|
|
33
|
+
// node --version works cross-platform
|
|
34
|
+
const result = await ToolDetector.execQuiet('node --version');
|
|
35
|
+
expect(result).toMatch(/^v\d+/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should reject for an invalid command', async () => {
|
|
39
|
+
await expect(
|
|
40
|
+
ToolDetector.execQuiet('nonexistent_binary_xyz_12345 --version')
|
|
41
|
+
).rejects.toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('detectBinary', () => {
|
|
46
|
+
test('should detect node binary (always available)', async () => {
|
|
47
|
+
const result = await ToolDetector.detectBinary('node');
|
|
48
|
+
expect(result.found).toBe(true);
|
|
49
|
+
expect(result.path).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should not detect nonexistent binary', async () => {
|
|
53
|
+
const result = await ToolDetector.detectBinary('nonexistent_tool_xyz_12345');
|
|
54
|
+
expect(result.found).toBe(false);
|
|
55
|
+
expect(result.path).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('detectProjectConfigs', () => {
|
|
60
|
+
let tmpDir;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
tmpDir = path.join(os.tmpdir(), `adf-test-${Date.now()}`);
|
|
64
|
+
await fs.ensureDir(tmpDir);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(async () => {
|
|
68
|
+
await fs.remove(tmpDir);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should detect existing project config files', async () => {
|
|
72
|
+
await fs.writeFile(path.join(tmpDir, '.cursorrules'), '# rules');
|
|
73
|
+
const result = await ToolDetector.detectProjectConfigs(tmpDir, ['.cursorrules', '.cursor/rules/']);
|
|
74
|
+
expect(result.found).toBe(true);
|
|
75
|
+
expect(result.files).toContain('.cursorrules');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should detect existing config directories', async () => {
|
|
79
|
+
await fs.ensureDir(path.join(tmpDir, '.windsurf', 'rules'));
|
|
80
|
+
const result = await ToolDetector.detectProjectConfigs(tmpDir, ['.windsurfrules', '.windsurf/rules/']);
|
|
81
|
+
expect(result.found).toBe(true);
|
|
82
|
+
expect(result.files).toContain('.windsurf/rules/');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should return not found for missing configs', async () => {
|
|
86
|
+
const result = await ToolDetector.detectProjectConfigs(tmpDir, ['.cursorrules', '.cursor/rules/']);
|
|
87
|
+
expect(result.found).toBe(false);
|
|
88
|
+
expect(result.files).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('detectConfigDirs', () => {
|
|
93
|
+
test('should detect existing directory', async () => {
|
|
94
|
+
// os.homedir() always exists
|
|
95
|
+
const result = await ToolDetector.detectConfigDirs([os.homedir()]);
|
|
96
|
+
expect(result.found).toBe(true);
|
|
97
|
+
expect(result.path).toBe(os.homedir());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should return not found for missing directories', async () => {
|
|
101
|
+
const result = await ToolDetector.detectConfigDirs(['/nonexistent_path_xyz_12345/']);
|
|
102
|
+
expect(result.found).toBe(false);
|
|
103
|
+
expect(result.path).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('detectAll', () => {
|
|
108
|
+
let tmpDir;
|
|
109
|
+
|
|
110
|
+
beforeEach(async () => {
|
|
111
|
+
tmpDir = path.join(os.tmpdir(), `adf-detect-all-${Date.now()}`);
|
|
112
|
+
await fs.ensureDir(tmpDir);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(async () => {
|
|
116
|
+
await fs.remove(tmpDir);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should return a Map with entries for all tools', async () => {
|
|
120
|
+
const results = await ToolDetector.detectAll(tmpDir);
|
|
121
|
+
expect(results).toBeInstanceOf(Map);
|
|
122
|
+
expect(results.size).toBeGreaterThanOrEqual(10);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('each entry should have installed, method, and path fields', async () => {
|
|
126
|
+
const results = await ToolDetector.detectAll(tmpDir);
|
|
127
|
+
for (const [, entry] of results) {
|
|
128
|
+
expect(entry).toHaveProperty('installed');
|
|
129
|
+
expect(entry).toHaveProperty('method');
|
|
130
|
+
expect(entry).toHaveProperty('path');
|
|
131
|
+
expect(typeof entry.installed).toBe('boolean');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('should detect tools via project config if present', async () => {
|
|
136
|
+
// Use a tool unlikely to be installed as a binary: trae
|
|
137
|
+
await fs.ensureDir(path.join(tmpDir, '.trae'));
|
|
138
|
+
await fs.writeJson(path.join(tmpDir, '.trae', 'config.json'), { test: true });
|
|
139
|
+
const results = await ToolDetector.detectAll(tmpDir);
|
|
140
|
+
const trae = results.get('trae');
|
|
141
|
+
// If trae binary is found it'll be 'binary', otherwise 'project-config'
|
|
142
|
+
expect(trae.installed).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('a2a protocol entry should not be marked installed', async () => {
|
|
146
|
+
const results = await ToolDetector.detectAll(tmpDir);
|
|
147
|
+
const a2a = results.get('a2a');
|
|
148
|
+
expect(a2a.installed).toBe(false);
|
|
149
|
+
expect(a2a.method).toBe('protocol');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|