acp-vscode 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +80 -0
- package/README.md +25 -5
- package/__tests__/fetcher-nested-and-default.test.js +461 -0
- package/__tests__/fetcher-verbose-integration.test.js +274 -0
- package/__tests__/fetcher-verbose.test.js +316 -0
- package/__tests__/fetcher.test.js +5 -5
- package/package.json +1 -1
- package/scripts/quality-checks.sh +93 -0
- package/src/commands/install.js +1 -1
- package/src/commands/list.js +1 -1
- package/src/commands/search.js +1 -1
- package/src/fetcher.js +144 -27
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
jest.mock('axios');
|
|
6
|
+
|
|
7
|
+
let fetcher;
|
|
8
|
+
let fetchIndex;
|
|
9
|
+
let diskPaths;
|
|
10
|
+
const cache = require('../src/cache');
|
|
11
|
+
|
|
12
|
+
describe('fetcher verbose logging integration tests', () => {
|
|
13
|
+
const tmp = path.join(os.tmpdir(), `acp-fetcher-integration-${Date.now()}`);
|
|
14
|
+
const origCwd = process.cwd();
|
|
15
|
+
let origNodeEnv;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
await fs.ensureDir(tmp);
|
|
19
|
+
process.chdir(tmp);
|
|
20
|
+
origNodeEnv = process.env.NODE_ENV;
|
|
21
|
+
process.env.NODE_ENV = 'test';
|
|
22
|
+
// require after chdir so diskPaths uses tmp cwd
|
|
23
|
+
fetcher = require('../src/fetcher');
|
|
24
|
+
fetchIndex = fetcher.fetchIndex;
|
|
25
|
+
diskPaths = fetcher.diskPaths;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
process.chdir(origCwd);
|
|
30
|
+
if (origNodeEnv === undefined) {
|
|
31
|
+
delete process.env.NODE_ENV;
|
|
32
|
+
} else {
|
|
33
|
+
process.env.NODE_ENV = origNodeEnv;
|
|
34
|
+
}
|
|
35
|
+
await fs.remove(tmp);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
cache.del('index');
|
|
40
|
+
const { DISK_CACHE_DIR } = diskPaths();
|
|
41
|
+
await fs.remove(DISK_CACHE_DIR).catch(() => {});
|
|
42
|
+
axios.get.mockReset();
|
|
43
|
+
// Clear env vars
|
|
44
|
+
delete process.env.ACP_INDEX_JSON;
|
|
45
|
+
delete process.env.ACP_REPOS_JSON;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('verbose logging works through complete flow: env var -> parse -> fetch -> cache', async () => {
|
|
49
|
+
const testRepoConfig = [
|
|
50
|
+
{
|
|
51
|
+
id: 'test-acp-cli',
|
|
52
|
+
treeUrl: 'https://api.github.com/repos/AndyG-0/test-acp-cli/git/trees/main?recursive=1',
|
|
53
|
+
rawBase: 'https://raw.githubusercontent.com/AndyG-0/test-acp-cli/main'
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
57
|
+
|
|
58
|
+
const logs = [];
|
|
59
|
+
const originalLog = console.log;
|
|
60
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Mock the GitHub API responses
|
|
64
|
+
axios.get.mockImplementation((url) => {
|
|
65
|
+
if (url === 'https://api.github.com/repos/AndyG-0/test-acp-cli/git/trees/main?recursive=1') {
|
|
66
|
+
return Promise.resolve({
|
|
67
|
+
status: 200,
|
|
68
|
+
data: {
|
|
69
|
+
tree: [
|
|
70
|
+
{ type: 'blob', path: 'prompts/sample-prompt.prompt.md' },
|
|
71
|
+
{ type: 'blob', path: 'agents/sample-agent.agent.md' },
|
|
72
|
+
{ type: 'blob', path: 'instructions/sample-instructions.instructions.md' },
|
|
73
|
+
{ type: 'blob', path: 'skills/sample-skill/skill.md' },
|
|
74
|
+
{ type: 'tree', path: 'skills/sample-skill' }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Mock content fetches
|
|
80
|
+
if (url.includes('raw.githubusercontent.com')) {
|
|
81
|
+
return Promise.resolve({
|
|
82
|
+
status: 200,
|
|
83
|
+
data: '---\ntitle: "Test Sample"\nversion: 1.0.0\n---\nContent here'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return Promise.reject(new Error('Unexpected URL: ' + url));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await fetchIndex({ verbose: true });
|
|
90
|
+
|
|
91
|
+
// Verify the verbose logs captured key events
|
|
92
|
+
expect(logs.some(l => l.includes('fetchIndex called'))).toBe(true);
|
|
93
|
+
expect(logs.some(l => l.includes('ACP_REPOS_JSON environment variable detected'))).toBe(true);
|
|
94
|
+
expect(logs.some(l => l.includes('successfully parsed ACP_REPOS_JSON'))).toBe(true);
|
|
95
|
+
expect(logs.some(l => l.includes('using repos from ACP_REPOS_JSON: test-acp-cli'))).toBe(true);
|
|
96
|
+
expect(logs.some(l => l.includes('fetching tree for repo: test-acp-cli'))).toBe(true);
|
|
97
|
+
expect(logs.some(l => l.includes('successfully fetched tree for repo: test-acp-cli'))).toBe(true);
|
|
98
|
+
expect(logs.some(l => l.includes('blob items'))).toBe(true);
|
|
99
|
+
expect(logs.some(l => l.includes('prompts'))).toBe(true);
|
|
100
|
+
expect(logs.some(l => l.includes('agents'))).toBe(true);
|
|
101
|
+
expect(logs.some(l => l.includes('combined index contains:'))).toBe(true);
|
|
102
|
+
expect(logs.some(l => l.includes('disk cache written'))).toBe(true);
|
|
103
|
+
expect(logs.some(l => l.includes('index successfully built and cached'))).toBe(true);
|
|
104
|
+
|
|
105
|
+
// Verify the index was built correctly
|
|
106
|
+
expect(result).toHaveProperty('prompts');
|
|
107
|
+
expect(result).toHaveProperty('agents');
|
|
108
|
+
expect(result).toHaveProperty('instructions');
|
|
109
|
+
expect(result).toHaveProperty('skills');
|
|
110
|
+
expect(result).toHaveProperty('_repos');
|
|
111
|
+
// Now includes both the default awesome-copilot repo and test-acp-cli
|
|
112
|
+
expect(result._repos.length).toBeGreaterThanOrEqual(2);
|
|
113
|
+
expect(result._repos.some(r => r.id === 'test-acp-cli')).toBe(true);
|
|
114
|
+
expect(result._repos.some(r => r.id === 'awesome-copilot')).toBe(true);
|
|
115
|
+
} finally {
|
|
116
|
+
console.log = originalLog;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('verbose logging shows configuration source priority', async () => {
|
|
121
|
+
// Test 1: With ACP_REPOS_JSON env var (highest priority)
|
|
122
|
+
const testRepoConfig = [
|
|
123
|
+
{ id: 'from-env', treeUrl: 'https://api.test/env', rawBase: 'https://raw.test/env' }
|
|
124
|
+
];
|
|
125
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
126
|
+
|
|
127
|
+
const logs1 = [];
|
|
128
|
+
const originalLog = console.log;
|
|
129
|
+
console.log = jest.fn(msg => logs1.push(msg));
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
axios.get.mockResolvedValue({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] } });
|
|
133
|
+
|
|
134
|
+
await fetchIndex({ verbose: true });
|
|
135
|
+
|
|
136
|
+
expect(logs1.some(l => l.includes('ACP_REPOS_JSON environment variable detected'))).toBe(true);
|
|
137
|
+
expect(logs1.some(l => l.includes('from-env'))).toBe(true);
|
|
138
|
+
} finally {
|
|
139
|
+
console.log = originalLog;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Clear env var and test file-based config
|
|
143
|
+
delete process.env.ACP_REPOS_JSON;
|
|
144
|
+
cache.del('index');
|
|
145
|
+
const { DISK_CACHE_DIR } = diskPaths();
|
|
146
|
+
await fs.remove(DISK_CACHE_DIR);
|
|
147
|
+
|
|
148
|
+
// Test 2: With acp-repos.json file (second priority)
|
|
149
|
+
const baseDir = process.cwd();
|
|
150
|
+
const repoFile = path.join(baseDir, 'acp-repos.json');
|
|
151
|
+
const fileRepoConfig = [
|
|
152
|
+
{ id: 'from-file', treeUrl: 'https://api.test/file', rawBase: 'https://raw.test/file' }
|
|
153
|
+
];
|
|
154
|
+
await fs.writeJson(repoFile, fileRepoConfig);
|
|
155
|
+
|
|
156
|
+
const logs2 = [];
|
|
157
|
+
console.log = jest.fn(msg => logs2.push(msg));
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
axios.get.mockResolvedValue({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] } });
|
|
161
|
+
|
|
162
|
+
await fetchIndex({ verbose: true });
|
|
163
|
+
|
|
164
|
+
expect(logs2.some(l => l.includes('acp-repos.json found'))).toBe(true);
|
|
165
|
+
expect(logs2.some(l => l.includes('from-file'))).toBe(true);
|
|
166
|
+
} finally {
|
|
167
|
+
console.log = originalLog;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Clear file and test default config
|
|
171
|
+
await fs.remove(repoFile);
|
|
172
|
+
cache.del('index');
|
|
173
|
+
await fs.remove(DISK_CACHE_DIR);
|
|
174
|
+
|
|
175
|
+
// Test 3: With defaults (lowest priority)
|
|
176
|
+
const logs3 = [];
|
|
177
|
+
console.log = jest.fn(msg => logs3.push(msg));
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
axios.get.mockResolvedValue({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] } });
|
|
181
|
+
|
|
182
|
+
await fetchIndex({ verbose: true });
|
|
183
|
+
|
|
184
|
+
expect(logs3.some(l => l.includes('awesome-copilot'))).toBe(true);
|
|
185
|
+
// Now the final repos list should include awesome-copilot plus any configured repos
|
|
186
|
+
expect(logs3.some(l => l.includes('final repos list:'))).toBe(true);
|
|
187
|
+
} finally {
|
|
188
|
+
console.log = originalLog;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('verbose logging captures and reports specific repo fetch errors', async () => {
|
|
193
|
+
const testRepoConfig = [
|
|
194
|
+
{
|
|
195
|
+
id: 'good-repo',
|
|
196
|
+
treeUrl: 'https://api.test/good',
|
|
197
|
+
rawBase: 'https://raw.test/good'
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'bad-repo-invalid-json',
|
|
201
|
+
treeUrl: 'https://api.test/badjson',
|
|
202
|
+
rawBase: 'https://raw.test/badjson'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'bad-repo-network',
|
|
206
|
+
treeUrl: 'https://api.test/badnetwork',
|
|
207
|
+
rawBase: 'https://raw.test/badnetwork'
|
|
208
|
+
}
|
|
209
|
+
];
|
|
210
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
211
|
+
|
|
212
|
+
const logs = [];
|
|
213
|
+
const originalLog = console.log;
|
|
214
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
axios.get.mockImplementation((url) => {
|
|
218
|
+
if (url === 'https://api.test/good') {
|
|
219
|
+
return Promise.resolve({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] } });
|
|
220
|
+
}
|
|
221
|
+
if (url === 'https://api.test/badjson') {
|
|
222
|
+
return Promise.resolve({ status: 500, data: 'Server error' });
|
|
223
|
+
}
|
|
224
|
+
if (url === 'https://api.test/badnetwork') {
|
|
225
|
+
return Promise.reject(new Error('Network timeout'));
|
|
226
|
+
}
|
|
227
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await fetchIndex({ verbose: true });
|
|
231
|
+
|
|
232
|
+
// Verify we see success for good repo
|
|
233
|
+
expect(logs.some(l => l.includes('fetching tree for repo: good-repo'))).toBe(true);
|
|
234
|
+
expect(logs.some(l => l.includes('successfully fetched tree for repo: good-repo'))).toBe(true);
|
|
235
|
+
|
|
236
|
+
// Verify we see error for bad repos
|
|
237
|
+
expect(logs.some(l => l.includes('fetching tree for repo: bad-repo-invalid-json'))).toBe(true);
|
|
238
|
+
expect(logs.some(l => l.includes('fetching tree for repo: bad-repo-network'))).toBe(true);
|
|
239
|
+
expect(logs.some(l => l.includes('failed to fetch tree'))).toBe(true);
|
|
240
|
+
} finally {
|
|
241
|
+
console.log = originalLog;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('verbose logs show transition from disk cache to fresh fetch', async () => {
|
|
246
|
+
const logs1 = [];
|
|
247
|
+
const originalLog = console.log;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// First fetch - write fresh cache
|
|
251
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
252
|
+
|
|
253
|
+
console.log = jest.fn(msg => logs1.push(msg));
|
|
254
|
+
const result1 = await fetchIndex({ verbose: true });
|
|
255
|
+
|
|
256
|
+
expect(logs1.some(l => l.includes('disk cache written'))).toBe(true);
|
|
257
|
+
expect(result1.prompts).toHaveLength(1);
|
|
258
|
+
|
|
259
|
+
// Second fetch - use in-memory cache
|
|
260
|
+
cache.del('index'); // Clear in-memory but disk cache still valid
|
|
261
|
+
logs1.length = 0;
|
|
262
|
+
|
|
263
|
+
console.log = jest.fn(msg => logs1.push(msg));
|
|
264
|
+
const result2 = await fetchIndex({ verbose: true });
|
|
265
|
+
|
|
266
|
+
expect(logs1.some(l => l.includes('reading disk cache from'))).toBe(true);
|
|
267
|
+
expect(logs1.some(l => l.includes('disk cache read successfully'))).toBe(true);
|
|
268
|
+
expect(logs1.some(l => l.includes('using fresh disk cache'))).toBe(true);
|
|
269
|
+
expect(result2.prompts).toHaveLength(1);
|
|
270
|
+
} finally {
|
|
271
|
+
console.log = originalLog;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
jest.mock('axios');
|
|
6
|
+
|
|
7
|
+
let fetcher;
|
|
8
|
+
let fetchIndex;
|
|
9
|
+
let diskPaths;
|
|
10
|
+
const cache = require('../src/cache');
|
|
11
|
+
|
|
12
|
+
describe('fetcher verbose logging', () => {
|
|
13
|
+
const tmp = path.join(os.tmpdir(), `acp-fetcher-verbose-${Date.now()}`);
|
|
14
|
+
const origCwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
await fs.ensureDir(tmp);
|
|
18
|
+
process.chdir(tmp);
|
|
19
|
+
// require after chdir so diskPaths uses tmp cwd
|
|
20
|
+
fetcher = require('../src/fetcher');
|
|
21
|
+
fetchIndex = fetcher.fetchIndex;
|
|
22
|
+
diskPaths = fetcher.diskPaths;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
process.chdir(origCwd);
|
|
27
|
+
await fs.remove(tmp);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
cache.del('index');
|
|
32
|
+
const { DISK_CACHE_DIR } = diskPaths();
|
|
33
|
+
await fs.remove(DISK_CACHE_DIR).catch(() => {});
|
|
34
|
+
axios.get.mockReset();
|
|
35
|
+
// Clear env vars
|
|
36
|
+
delete process.env.ACP_INDEX_JSON;
|
|
37
|
+
delete process.env.ACP_REPOS_JSON;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('logs when using ACP_INDEX_JSON environment variable', async () => {
|
|
41
|
+
const indexData = { prompts: [{ id: 'p1', name: 'Prompt One' }] };
|
|
42
|
+
process.env.ACP_INDEX_JSON = JSON.stringify(indexData);
|
|
43
|
+
|
|
44
|
+
const logs = [];
|
|
45
|
+
const originalLog = console.log;
|
|
46
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await fetchIndex({ verbose: true });
|
|
50
|
+
|
|
51
|
+
expect(logs.some(l => l.includes('ACP_INDEX_JSON environment variable detected'))).toBe(true);
|
|
52
|
+
expect(logs.some(l => l.includes('successfully parsed ACP_INDEX_JSON'))).toBe(true);
|
|
53
|
+
} finally {
|
|
54
|
+
console.log = originalLog;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('logs error when ACP_INDEX_JSON is malformed', async () => {
|
|
59
|
+
process.env.ACP_INDEX_JSON = 'not valid json {';
|
|
60
|
+
|
|
61
|
+
const logs = [];
|
|
62
|
+
const originalLog = console.log;
|
|
63
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Mock the default repo call
|
|
67
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
68
|
+
|
|
69
|
+
await fetchIndex({ verbose: true });
|
|
70
|
+
|
|
71
|
+
expect(logs.some(l => l.includes('ACP_INDEX_JSON environment variable detected'))).toBe(true);
|
|
72
|
+
expect(logs.some(l => l.includes('failed to parse ACP_INDEX_JSON'))).toBe(true);
|
|
73
|
+
} finally {
|
|
74
|
+
console.log = originalLog;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('logs when using ACP_REPOS_JSON environment variable', async () => {
|
|
79
|
+
const reposData = [
|
|
80
|
+
{ id: 'test-repo', treeUrl: 'https://api.github.test/repo1', rawBase: 'https://raw.test/repo1' }
|
|
81
|
+
];
|
|
82
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(reposData);
|
|
83
|
+
|
|
84
|
+
const logs = [];
|
|
85
|
+
const originalLog = console.log;
|
|
86
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
axios.get.mockResolvedValue({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p1.prompt.md' }] } });
|
|
90
|
+
|
|
91
|
+
await fetchIndex({ verbose: true });
|
|
92
|
+
|
|
93
|
+
expect(logs.some(l => l.includes('ACP_REPOS_JSON environment variable detected'))).toBe(true);
|
|
94
|
+
expect(logs.some(l => l.includes('successfully parsed ACP_REPOS_JSON with 1 repo(s)'))).toBe(true);
|
|
95
|
+
expect(logs.some(l => l.includes('using repos from ACP_REPOS_JSON: test-repo'))).toBe(true);
|
|
96
|
+
} finally {
|
|
97
|
+
console.log = originalLog;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('logs error when ACP_REPOS_JSON is malformed', async () => {
|
|
102
|
+
process.env.ACP_REPOS_JSON = 'not valid json {';
|
|
103
|
+
|
|
104
|
+
const logs = [];
|
|
105
|
+
const originalLog = console.log;
|
|
106
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
110
|
+
|
|
111
|
+
await fetchIndex({ verbose: true });
|
|
112
|
+
|
|
113
|
+
expect(logs.some(l => l.includes('ACP_REPOS_JSON environment variable detected'))).toBe(true);
|
|
114
|
+
expect(logs.some(l => l.includes('failed to parse ACP_REPOS_JSON'))).toBe(true);
|
|
115
|
+
} finally {
|
|
116
|
+
console.log = originalLog;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('logs when loading acp-repos.json file', async () => {
|
|
121
|
+
const reposData = [
|
|
122
|
+
{ id: 'file-repo', treeUrl: 'https://api.github.test/repo2', rawBase: 'https://raw.test/repo2' }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const baseDir = process.cwd();
|
|
126
|
+
const repoFile = path.join(baseDir, 'acp-repos.json');
|
|
127
|
+
await fs.writeJson(repoFile, reposData);
|
|
128
|
+
|
|
129
|
+
const logs = [];
|
|
130
|
+
const originalLog = console.log;
|
|
131
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
axios.get.mockResolvedValue({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p1.prompt.md' }] } });
|
|
135
|
+
|
|
136
|
+
await fetchIndex({ verbose: true });
|
|
137
|
+
|
|
138
|
+
expect(logs.some(l => l.includes('checking for acp-repos.json at'))).toBe(true);
|
|
139
|
+
expect(logs.some(l => l.includes('acp-repos.json found, reading...'))).toBe(true);
|
|
140
|
+
expect(logs.some(l => l.includes('successfully loaded 1 repo(s) from acp-repos.json'))).toBe(true);
|
|
141
|
+
expect(logs.some(l => l.includes('using repos from acp-repos.json: file-repo'))).toBe(true);
|
|
142
|
+
} finally {
|
|
143
|
+
console.log = originalLog;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('logs error when acp-repos.json is malformed', async () => {
|
|
148
|
+
const baseDir = process.cwd();
|
|
149
|
+
const repoFile = path.join(baseDir, 'acp-repos.json');
|
|
150
|
+
await fs.writeFile(repoFile, 'not valid json {');
|
|
151
|
+
|
|
152
|
+
const logs = [];
|
|
153
|
+
const originalLog = console.log;
|
|
154
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
158
|
+
|
|
159
|
+
await fetchIndex({ verbose: true });
|
|
160
|
+
|
|
161
|
+
expect(logs.some(l => l.includes('checking for acp-repos.json at'))).toBe(true);
|
|
162
|
+
expect(logs.some(l => l.includes('failed to read acp-repos.json'))).toBe(true);
|
|
163
|
+
} finally {
|
|
164
|
+
console.log = originalLog;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('logs when using default repos', async () => {
|
|
169
|
+
const logs = [];
|
|
170
|
+
const originalLog = console.log;
|
|
171
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
axios.get.mockResolvedValue({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p1.prompt.md' }] } });
|
|
175
|
+
|
|
176
|
+
await fetchIndex({ verbose: true });
|
|
177
|
+
|
|
178
|
+
expect(logs.some(l => l.includes('checking for acp-repos.json at'))).toBe(true);
|
|
179
|
+
// The key assertion is that we either see "found" or "not found"
|
|
180
|
+
expect(logs.some(l => l.includes('acp-repos.json') && (l.includes('not found') || l.includes('found')))).toBe(true);
|
|
181
|
+
expect(logs.some(l => l.includes('final repos list:'))).toBe(true);
|
|
182
|
+
expect(logs.some(l => l.includes('awesome-copilot'))).toBe(true);
|
|
183
|
+
expect(logs.some(l => l.includes('fetching repos'))).toBe(true);
|
|
184
|
+
} finally {
|
|
185
|
+
console.log = originalLog;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('logs repo fetching successes and failures', async () => {
|
|
190
|
+
const reposData = [
|
|
191
|
+
{ id: 'success-repo', treeUrl: 'https://api.github.test/success', rawBase: 'https://raw.test/success' },
|
|
192
|
+
{ id: 'fail-repo', treeUrl: 'https://api.github.test/fail', rawBase: 'https://raw.test/fail' }
|
|
193
|
+
];
|
|
194
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(reposData);
|
|
195
|
+
|
|
196
|
+
const logs = [];
|
|
197
|
+
const originalLog = console.log;
|
|
198
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
axios.get.mockImplementation((url) => {
|
|
202
|
+
if (url === 'https://api.github.test/success') {
|
|
203
|
+
return Promise.resolve({ status: 200, data: { tree: [{ type: 'blob', path: 'prompts/p1.prompt.md' }] } });
|
|
204
|
+
}
|
|
205
|
+
if (url === 'https://api.github.test/fail') {
|
|
206
|
+
return Promise.reject(new Error('Network error'));
|
|
207
|
+
}
|
|
208
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await fetchIndex({ verbose: true });
|
|
212
|
+
|
|
213
|
+
expect(logs.some(l => l.includes('fetching tree for repo: success-repo'))).toBe(true);
|
|
214
|
+
expect(logs.some(l => l.includes('successfully fetched tree for repo: success-repo'))).toBe(true);
|
|
215
|
+
expect(logs.some(l => l.includes('fetching tree for repo: fail-repo'))).toBe(true);
|
|
216
|
+
expect(logs.some(l => l.includes('failed to fetch tree for repo fail-repo'))).toBe(true);
|
|
217
|
+
} finally {
|
|
218
|
+
console.log = originalLog;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('logs item count summary', async () => {
|
|
223
|
+
const logs = [];
|
|
224
|
+
const originalLog = console.log;
|
|
225
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
axios.get.mockImplementation((url) => {
|
|
229
|
+
if (url.includes('tree')) {
|
|
230
|
+
return Promise.resolve({
|
|
231
|
+
status: 200,
|
|
232
|
+
data: {
|
|
233
|
+
tree: [
|
|
234
|
+
{ type: 'blob', path: 'prompts/p1.prompt.md' },
|
|
235
|
+
{ type: 'blob', path: 'agents/a1.agent.md' }
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await fetchIndex({ verbose: true });
|
|
244
|
+
|
|
245
|
+
expect(logs.some(l => l.includes('combined index contains:'))).toBe(true);
|
|
246
|
+
expect(logs.some(l => l.includes('prompts:'))).toBe(true);
|
|
247
|
+
expect(logs.some(l => l.includes('agents:'))).toBe(true);
|
|
248
|
+
} finally {
|
|
249
|
+
console.log = originalLog;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('does not log when verbose is false', async () => {
|
|
254
|
+
const logs = [];
|
|
255
|
+
const originalLog = console.log;
|
|
256
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
260
|
+
|
|
261
|
+
await fetchIndex({ verbose: false });
|
|
262
|
+
|
|
263
|
+
// Should not have verbose logs (only warnings about baseline-browser-mapping)
|
|
264
|
+
const verboseLogs = logs.filter(l => l.includes('verbose:'));
|
|
265
|
+
expect(verboseLogs.length).toBe(0);
|
|
266
|
+
} finally {
|
|
267
|
+
console.log = originalLog;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('does not log when options is not provided', async () => {
|
|
272
|
+
const logs = [];
|
|
273
|
+
const originalLog = console.log;
|
|
274
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
278
|
+
|
|
279
|
+
await fetchIndex();
|
|
280
|
+
|
|
281
|
+
// Should not have verbose logs
|
|
282
|
+
const verboseLogs = logs.filter(l => l.includes('verbose:'));
|
|
283
|
+
expect(verboseLogs.length).toBe(0);
|
|
284
|
+
} finally {
|
|
285
|
+
console.log = originalLog;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('logs disk cache operations', async () => {
|
|
290
|
+
const logs = [];
|
|
291
|
+
const originalLog = console.log;
|
|
292
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
axios.get.mockResolvedValue({ status: 200, data: { prompts: [{ id: 'p1' }] } });
|
|
296
|
+
|
|
297
|
+
// First call should write cache
|
|
298
|
+
await fetchIndex({ verbose: true });
|
|
299
|
+
|
|
300
|
+
expect(logs.some(l => l.includes('disk cache written to'))).toBe(true);
|
|
301
|
+
|
|
302
|
+
// Clear in-memory cache to force disk read on second call
|
|
303
|
+
cache.del('index');
|
|
304
|
+
logs.length = 0;
|
|
305
|
+
|
|
306
|
+
// Second call should read from disk
|
|
307
|
+
await fetchIndex({ verbose: true });
|
|
308
|
+
|
|
309
|
+
expect(logs.some(l => l.includes('reading disk cache from'))).toBe(true);
|
|
310
|
+
expect(logs.some(l => l.includes('disk cache read successfully'))).toBe(true);
|
|
311
|
+
expect(logs.some(l => l.includes('using fresh disk cache'))).toBe(true);
|
|
312
|
+
} finally {
|
|
313
|
+
console.log = originalLog;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -99,11 +99,11 @@ describe('fetcher', () => {
|
|
|
99
99
|
|
|
100
100
|
const idx = await fetchIndex();
|
|
101
101
|
|
|
102
|
-
// repos metadata should be present
|
|
103
|
-
expect(idx._repos).
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
// repos metadata should be present - now includes the default awesome-copilot repo
|
|
103
|
+
expect(idx._repos).toHaveLength(3);
|
|
104
|
+
expect(idx._repos.map(r => r.id)).toEqual(expect.arrayContaining(['awesome-copilot', 'r1', 'r2']));
|
|
105
|
+
expect(idx._repos.some(r => r.id === 'r1' && r.treeUrl === 'https://api/repo1')).toBe(true);
|
|
106
|
+
expect(idx._repos.some(r => r.id === 'r2' && r.treeUrl === 'https://api/repo2')).toBe(true);
|
|
107
107
|
|
|
108
108
|
// prompts should include items from both repos
|
|
109
109
|
const pids = idx.prompts.map(p => `${p.repo}:${p.id}`).sort();
|
package/package.json
CHANGED