opencode-pilot 0.5.1 → 0.6.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/AGENTS.md +10 -2
- package/bin/opencode-pilot +29 -36
- package/examples/config.yaml +17 -1
- package/package.json +1 -1
- package/service/poll-service.js +33 -9
- package/service/presets/github.yaml +3 -0
- package/service/repo-config.js +19 -8
- package/test/unit/poll-service.test.js +138 -0
- package/test/unit/repo-config.test.js +47 -2
package/AGENTS.md
CHANGED
|
@@ -36,7 +36,15 @@ gh release list -R athal7/opencode-pilot -L 1
|
|
|
36
36
|
npm view opencode-pilot version
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
### 3.
|
|
39
|
+
### 3. Clear OpenCode Plugin Cache
|
|
40
|
+
|
|
41
|
+
OpenCode caches plugins at `~/.cache/opencode/node_modules/`. Clear the cache to pick up the new version:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rm -rf ~/.cache/opencode/node_modules/opencode-pilot
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 4. Restart Service
|
|
40
48
|
|
|
41
49
|
If the service is running, restart it:
|
|
42
50
|
|
|
@@ -45,7 +53,7 @@ If the service is running, restart it:
|
|
|
45
53
|
npx opencode-pilot start
|
|
46
54
|
```
|
|
47
55
|
|
|
48
|
-
###
|
|
56
|
+
### 5. Verify Upgrade
|
|
49
57
|
|
|
50
58
|
```bash
|
|
51
59
|
npx opencode-pilot status
|
package/bin/opencode-pilot
CHANGED
|
@@ -361,30 +361,29 @@ async function configCommand() {
|
|
|
361
361
|
// ============================================================================
|
|
362
362
|
|
|
363
363
|
async function testSourceCommand(sourceName) {
|
|
364
|
+
// Load sources using getSources() to expand presets
|
|
365
|
+
const { loadRepoConfig, getSources } = await import(join(serviceDir, "repo-config.js"));
|
|
366
|
+
loadRepoConfig(PILOT_CONFIG_FILE);
|
|
367
|
+
|
|
368
|
+
let sources;
|
|
369
|
+
try {
|
|
370
|
+
sources = getSources();
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.error(`Failed to load sources: ${err.message}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
364
376
|
if (!sourceName) {
|
|
365
377
|
console.error("Usage: opencode-pilot test-source <source-name>");
|
|
366
378
|
console.error("");
|
|
367
379
|
console.error("Available sources:");
|
|
368
380
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
const { default: YAML } = await import("yaml");
|
|
373
|
-
const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
|
|
374
|
-
const config = YAML.parse(content);
|
|
375
|
-
const sources = config.sources || [];
|
|
376
|
-
if (sources.length === 0) {
|
|
377
|
-
console.error(" (no sources configured)");
|
|
378
|
-
} else {
|
|
379
|
-
for (const s of sources) {
|
|
380
|
-
console.error(` ${s.name}`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
} catch (err) {
|
|
384
|
-
console.error(` Error loading config: ${err.message}`);
|
|
385
|
-
}
|
|
381
|
+
if (sources.length === 0) {
|
|
382
|
+
console.error(" (no sources configured)");
|
|
386
383
|
} else {
|
|
387
|
-
|
|
384
|
+
for (const s of sources) {
|
|
385
|
+
console.error(` ${s.name}`);
|
|
386
|
+
}
|
|
388
387
|
}
|
|
389
388
|
process.exit(1);
|
|
390
389
|
}
|
|
@@ -393,18 +392,7 @@ async function testSourceCommand(sourceName) {
|
|
|
393
392
|
console.log("=".repeat(40));
|
|
394
393
|
console.log("");
|
|
395
394
|
|
|
396
|
-
// Load config
|
|
397
|
-
if (!existsSync(PILOT_CONFIG_FILE)) {
|
|
398
|
-
console.error(`Config file not found: ${PILOT_CONFIG_FILE}`);
|
|
399
|
-
process.exit(1);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const { default: YAML } = await import("yaml");
|
|
403
|
-
const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
|
|
404
|
-
const config = YAML.parse(content);
|
|
405
|
-
|
|
406
395
|
// Find source
|
|
407
|
-
const sources = config.sources || [];
|
|
408
396
|
const source = sources.find(s => s.name === sourceName);
|
|
409
397
|
|
|
410
398
|
if (!source) {
|
|
@@ -429,19 +417,24 @@ async function testSourceCommand(sourceName) {
|
|
|
429
417
|
if (source.prompt) console.log(` prompt: ${source.prompt}`);
|
|
430
418
|
if (source.agent) console.log(` agent: ${source.agent}`);
|
|
431
419
|
|
|
432
|
-
// Show mappings
|
|
433
|
-
const
|
|
420
|
+
// Show mappings (using getToolProviderConfig which includes preset defaults)
|
|
421
|
+
const { getToolProviderConfig } = await import(join(serviceDir, "repo-config.js"));
|
|
434
422
|
const provider = source.tool?.mcp;
|
|
435
|
-
const
|
|
423
|
+
const toolProviderConfig = getToolProviderConfig(provider);
|
|
424
|
+
const mappings = toolProviderConfig?.mappings;
|
|
436
425
|
|
|
437
426
|
console.log("");
|
|
438
427
|
console.log("Field Mappings:");
|
|
439
|
-
if (mappings) {
|
|
428
|
+
if (mappings && Object.keys(mappings).length > 0) {
|
|
440
429
|
for (const [target, sourcePath] of Object.entries(mappings)) {
|
|
441
430
|
console.log(` ${target} ← ${sourcePath}`);
|
|
442
431
|
}
|
|
443
432
|
} else {
|
|
444
|
-
console.log(` (no mappings
|
|
433
|
+
console.log(` (no mappings for provider '${provider}')`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (toolProviderConfig?.response_key) {
|
|
437
|
+
console.log(` response_key: ${toolProviderConfig.response_key}`);
|
|
445
438
|
}
|
|
446
439
|
|
|
447
440
|
// Fetch items
|
|
@@ -453,8 +446,8 @@ async function testSourceCommand(sourceName) {
|
|
|
453
446
|
const pollerPath = join(serviceDir, "poller.js");
|
|
454
447
|
const { pollGenericSource, applyMappings } = await import(pollerPath);
|
|
455
448
|
|
|
456
|
-
// Fetch items with mappings
|
|
457
|
-
const items = await pollGenericSource(source, {
|
|
449
|
+
// Fetch items with tool provider config (includes mappings and response_key)
|
|
450
|
+
const items = await pollGenericSource(source, { toolProviderConfig });
|
|
458
451
|
|
|
459
452
|
console.log(`Found ${items.length} item(s)`);
|
|
460
453
|
console.log("");
|
package/examples/config.yaml
CHANGED
|
@@ -29,9 +29,12 @@ sources:
|
|
|
29
29
|
- preset: github/review-requests
|
|
30
30
|
prompt: review
|
|
31
31
|
|
|
32
|
-
# My PRs that have change requests
|
|
32
|
+
# My PRs that have change requests (filter to specific repos)
|
|
33
33
|
- preset: github/my-prs-feedback
|
|
34
34
|
prompt: review-feedback
|
|
35
|
+
repos: # Optional: filter to specific repos
|
|
36
|
+
- myorg/backend
|
|
37
|
+
- myorg/frontend
|
|
35
38
|
|
|
36
39
|
# Linear issues (requires teamId and assigneeId)
|
|
37
40
|
# Find IDs via: opencode-pilot discover linear
|
|
@@ -82,6 +85,19 @@ sources:
|
|
|
82
85
|
# title: name
|
|
83
86
|
# body: notes
|
|
84
87
|
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# REPOS - Map GitHub repos to local paths (OPTIONAL)
|
|
90
|
+
#
|
|
91
|
+
# GitHub presets automatically resolve the repo from each item's
|
|
92
|
+
# repository.full_name field. Add entries here to map repos to local paths.
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# repos:
|
|
95
|
+
# myorg/backend:
|
|
96
|
+
# path: ~/code/backend
|
|
97
|
+
#
|
|
98
|
+
# myorg/frontend:
|
|
99
|
+
# path: ~/code/frontend
|
|
100
|
+
|
|
85
101
|
# =============================================================================
|
|
86
102
|
# Available presets (no tools config needed):
|
|
87
103
|
# github/my-issues - Issues assigned to me
|
package/package.json
CHANGED
package/service/poll-service.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 5. Track processed items to avoid duplicates
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { loadRepoConfig, getRepoConfig, getAllSources, getToolProviderConfig } from "./repo-config.js";
|
|
12
|
+
import { loadRepoConfig, getRepoConfig, getAllSources, getToolProviderConfig, resolveRepoForItem } from "./repo-config.js";
|
|
13
13
|
import { createPoller, pollGenericSource } from "./poller.js";
|
|
14
14
|
import { evaluateReadiness, sortByPriority } from "./readiness.js";
|
|
15
15
|
import { executeAction, buildCommand } from "./actions.js";
|
|
@@ -52,6 +52,25 @@ export function buildActionConfigFromSource(source, repoConfig) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Build action config for a specific item, resolving repo from item fields
|
|
57
|
+
* Uses source.repo template (e.g., "{repository.full_name}") to look up repo config
|
|
58
|
+
* @param {object} source - Source configuration
|
|
59
|
+
* @param {object} item - Item from the source (contains repo info)
|
|
60
|
+
* @returns {object} Merged action config
|
|
61
|
+
*/
|
|
62
|
+
export function buildActionConfigForItem(source, item) {
|
|
63
|
+
// Resolve repo key from item using source.repo template
|
|
64
|
+
const repoKeys = resolveRepoForItem(source, item);
|
|
65
|
+
const repoKey = repoKeys.length > 0 ? repoKeys[0] : null;
|
|
66
|
+
|
|
67
|
+
// Get repo config (returns empty object if repo not configured)
|
|
68
|
+
const repoConfig = repoKey ? getRepoConfig(repoKey) : {};
|
|
69
|
+
|
|
70
|
+
// Build config with repo config as base, source overrides on top
|
|
71
|
+
return buildActionConfigFromSource(source, repoConfig);
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
// Global state
|
|
56
75
|
let pollingInterval = null;
|
|
57
76
|
let pollerInstance = null;
|
|
@@ -87,8 +106,6 @@ export async function pollOnce(options = {}) {
|
|
|
87
106
|
// Process each source
|
|
88
107
|
for (const source of sources) {
|
|
89
108
|
const sourceName = source.name || 'unknown';
|
|
90
|
-
const repoKey = source.name || 'default';
|
|
91
|
-
const repoConfig = getRepoConfig(repoKey) || {};
|
|
92
109
|
|
|
93
110
|
if (!hasToolConfig(source)) {
|
|
94
111
|
console.error(`[poll] Source '${sourceName}' missing tool configuration (requires tool.mcp and tool.name)`);
|
|
@@ -112,21 +129,28 @@ export async function pollOnce(options = {}) {
|
|
|
112
129
|
// Evaluate readiness and filter
|
|
113
130
|
const readyItems = items
|
|
114
131
|
.map((item) => {
|
|
132
|
+
// Resolve repo from item for per-item config
|
|
133
|
+
const repoKeys = resolveRepoForItem(source, item);
|
|
134
|
+
const repoKey = repoKeys.length > 0 ? repoKeys[0] : null;
|
|
135
|
+
const repoConfig = repoKey ? getRepoConfig(repoKey) : {};
|
|
136
|
+
|
|
115
137
|
const readiness = evaluateReadiness(item, repoConfig);
|
|
116
138
|
debug(`Item ${item.id}: ready=${readiness.ready}, reason=${readiness.reason || 'none'}`);
|
|
117
139
|
return {
|
|
118
140
|
...item,
|
|
119
|
-
repo_key: repoKey,
|
|
120
|
-
repo_short: repoKey.split("/").pop(),
|
|
141
|
+
repo_key: repoKey || sourceName,
|
|
142
|
+
repo_short: repoKey ? repoKey.split("/").pop() : sourceName,
|
|
121
143
|
_readiness: readiness,
|
|
144
|
+
_repoConfig: repoConfig,
|
|
122
145
|
};
|
|
123
146
|
})
|
|
124
147
|
.filter((item) => item._readiness.ready);
|
|
125
148
|
|
|
126
149
|
debug(`${readyItems.length} items ready out of ${items.length}`);
|
|
127
150
|
|
|
128
|
-
// Sort by priority
|
|
129
|
-
const
|
|
151
|
+
// Sort by priority (use first item's repo config or empty)
|
|
152
|
+
const sortConfig = readyItems.length > 0 ? readyItems[0]._repoConfig : {};
|
|
153
|
+
const sortedItems = sortByPriority(readyItems, sortConfig);
|
|
130
154
|
|
|
131
155
|
// Process ready items
|
|
132
156
|
debug(`Processing ${sortedItems.length} sorted items`);
|
|
@@ -138,8 +162,8 @@ export async function pollOnce(options = {}) {
|
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
debug(`Executing action for ${item.id}`);
|
|
141
|
-
// Build action config from source (
|
|
142
|
-
const actionConfig =
|
|
165
|
+
// Build action config from source and item (resolves repo from item fields)
|
|
166
|
+
const actionConfig = buildActionConfigForItem(source, item);
|
|
143
167
|
|
|
144
168
|
// Execute or dry-run
|
|
145
169
|
if (dryRun) {
|
|
@@ -15,6 +15,7 @@ my-issues:
|
|
|
15
15
|
q: "is:issue assignee:@me state:open"
|
|
16
16
|
item:
|
|
17
17
|
id: "{html_url}"
|
|
18
|
+
repo: "{repository.full_name}"
|
|
18
19
|
|
|
19
20
|
review-requests:
|
|
20
21
|
name: review-requests
|
|
@@ -25,6 +26,7 @@ review-requests:
|
|
|
25
26
|
q: "is:pr review-requested:@me state:open"
|
|
26
27
|
item:
|
|
27
28
|
id: "{html_url}"
|
|
29
|
+
repo: "{repository.full_name}"
|
|
28
30
|
|
|
29
31
|
my-prs-feedback:
|
|
30
32
|
name: my-prs-feedback
|
|
@@ -35,3 +37,4 @@ my-prs-feedback:
|
|
|
35
37
|
q: "is:pr author:@me state:open review:changes_requested"
|
|
36
38
|
item:
|
|
37
39
|
id: "{html_url}"
|
|
40
|
+
repo: "{repository.full_name}"
|
package/service/repo-config.js
CHANGED
|
@@ -234,20 +234,31 @@ export function getTemplate(templateName, templatesDir) {
|
|
|
234
234
|
* @returns {Array<string>} Array of repo keys
|
|
235
235
|
*/
|
|
236
236
|
export function resolveRepoForItem(source, item) {
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
return source.repos;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Single repo from field reference (e.g., "{repository.full_name}")
|
|
237
|
+
// Resolve repo from item using template (e.g., "{repository.full_name}")
|
|
238
|
+
let resolvedRepo = null;
|
|
243
239
|
if (typeof source.repo === "string") {
|
|
244
240
|
const resolved = expandTemplate(source.repo, item);
|
|
245
|
-
// Only
|
|
241
|
+
// Only use if actually resolved (not still a template)
|
|
246
242
|
if (resolved && !resolved.includes("{")) {
|
|
247
|
-
|
|
243
|
+
resolvedRepo = resolved;
|
|
248
244
|
}
|
|
249
245
|
}
|
|
250
246
|
|
|
247
|
+
// If source.repos is an array, use it as an allowlist filter
|
|
248
|
+
if (Array.isArray(source.repos)) {
|
|
249
|
+
// If we resolved a repo from the item, check if it's in the allowlist
|
|
250
|
+
if (resolvedRepo) {
|
|
251
|
+
return source.repos.includes(resolvedRepo) ? [resolvedRepo] : [];
|
|
252
|
+
}
|
|
253
|
+
// No repo template - return empty (can't match without item context)
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// No allowlist - return the resolved repo if we have one
|
|
258
|
+
if (resolvedRepo) {
|
|
259
|
+
return [resolvedRepo];
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
// No repo configuration - repo-agnostic source
|
|
252
263
|
return [];
|
|
253
264
|
}
|
|
@@ -139,4 +139,142 @@ sources:
|
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
describe('per-item repo resolution', () => {
|
|
144
|
+
test('resolves repo config from item using source.repo template', async () => {
|
|
145
|
+
const config = `
|
|
146
|
+
repos:
|
|
147
|
+
myorg/backend:
|
|
148
|
+
path: ~/code/backend
|
|
149
|
+
prompt: worktree
|
|
150
|
+
session:
|
|
151
|
+
name: "issue-{number}"
|
|
152
|
+
|
|
153
|
+
sources:
|
|
154
|
+
- preset: github/my-issues
|
|
155
|
+
`;
|
|
156
|
+
writeFileSync(configPath, config);
|
|
157
|
+
|
|
158
|
+
const { loadRepoConfig, getSources, getRepoConfig, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
159
|
+
loadRepoConfig(configPath);
|
|
160
|
+
|
|
161
|
+
const source = getSources()[0];
|
|
162
|
+
const item = {
|
|
163
|
+
repository: { full_name: 'myorg/backend' },
|
|
164
|
+
number: 123,
|
|
165
|
+
html_url: 'https://github.com/myorg/backend/issues/123'
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Source should have repo field from preset
|
|
169
|
+
assert.strictEqual(source.repo, '{repository.full_name}');
|
|
170
|
+
|
|
171
|
+
// resolveRepoForItem should extract repo key from item
|
|
172
|
+
const repoKeys = resolveRepoForItem(source, item);
|
|
173
|
+
assert.deepStrictEqual(repoKeys, ['myorg/backend']);
|
|
174
|
+
|
|
175
|
+
// getRepoConfig should return the repo settings
|
|
176
|
+
const repoConfig = getRepoConfig(repoKeys[0]);
|
|
177
|
+
assert.strictEqual(repoConfig.path, '~/code/backend');
|
|
178
|
+
assert.strictEqual(repoConfig.prompt, 'worktree');
|
|
179
|
+
assert.deepStrictEqual(repoConfig.session, { name: 'issue-{number}' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('falls back gracefully when repo not in config', async () => {
|
|
183
|
+
const config = `
|
|
184
|
+
repos:
|
|
185
|
+
myorg/backend:
|
|
186
|
+
path: ~/code/backend
|
|
187
|
+
|
|
188
|
+
sources:
|
|
189
|
+
- preset: github/my-issues
|
|
190
|
+
`;
|
|
191
|
+
writeFileSync(configPath, config);
|
|
192
|
+
|
|
193
|
+
const { loadRepoConfig, getSources, getRepoConfig, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
194
|
+
loadRepoConfig(configPath);
|
|
195
|
+
|
|
196
|
+
const source = getSources()[0];
|
|
197
|
+
const item = {
|
|
198
|
+
repository: { full_name: 'unknown/repo' },
|
|
199
|
+
number: 456
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// resolveRepoForItem should extract repo key from item
|
|
203
|
+
const repoKeys = resolveRepoForItem(source, item);
|
|
204
|
+
assert.deepStrictEqual(repoKeys, ['unknown/repo']);
|
|
205
|
+
|
|
206
|
+
// getRepoConfig should return empty object for unknown repo
|
|
207
|
+
const repoConfig = getRepoConfig(repoKeys[0]);
|
|
208
|
+
assert.deepStrictEqual(repoConfig, {});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('buildActionConfigForItem', () => {
|
|
213
|
+
test('uses repo config resolved from item', async () => {
|
|
214
|
+
const config = `
|
|
215
|
+
repos:
|
|
216
|
+
myorg/backend:
|
|
217
|
+
path: ~/code/backend
|
|
218
|
+
prompt: repo-prompt
|
|
219
|
+
session:
|
|
220
|
+
name: "issue-{number}"
|
|
221
|
+
|
|
222
|
+
sources:
|
|
223
|
+
- preset: github/my-issues
|
|
224
|
+
prompt: source-prompt
|
|
225
|
+
`;
|
|
226
|
+
writeFileSync(configPath, config);
|
|
227
|
+
|
|
228
|
+
const { loadRepoConfig } = await import('../../service/repo-config.js');
|
|
229
|
+
const { buildActionConfigForItem } = await import('../../service/poll-service.js');
|
|
230
|
+
loadRepoConfig(configPath);
|
|
231
|
+
|
|
232
|
+
const source = {
|
|
233
|
+
name: 'my-issues',
|
|
234
|
+
repo: '{repository.full_name}',
|
|
235
|
+
prompt: 'source-prompt'
|
|
236
|
+
};
|
|
237
|
+
const item = {
|
|
238
|
+
repository: { full_name: 'myorg/backend' },
|
|
239
|
+
number: 123
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const actionConfig = buildActionConfigForItem(source, item);
|
|
243
|
+
|
|
244
|
+
// Should use repo path from repos config
|
|
245
|
+
assert.strictEqual(actionConfig.repo_path, '~/code/backend');
|
|
246
|
+
// Source prompt should override repo prompt
|
|
247
|
+
assert.strictEqual(actionConfig.prompt, 'source-prompt');
|
|
248
|
+
// Session should come from repo config
|
|
249
|
+
assert.deepStrictEqual(actionConfig.session, { name: 'issue-{number}' });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('falls back to source working_dir when repo not configured', async () => {
|
|
253
|
+
const config = `
|
|
254
|
+
sources:
|
|
255
|
+
- preset: github/my-issues
|
|
256
|
+
working_dir: ~/default/path
|
|
257
|
+
`;
|
|
258
|
+
writeFileSync(configPath, config);
|
|
259
|
+
|
|
260
|
+
const { loadRepoConfig } = await import('../../service/repo-config.js');
|
|
261
|
+
const { buildActionConfigForItem } = await import('../../service/poll-service.js');
|
|
262
|
+
loadRepoConfig(configPath);
|
|
263
|
+
|
|
264
|
+
const source = {
|
|
265
|
+
name: 'my-issues',
|
|
266
|
+
repo: '{repository.full_name}',
|
|
267
|
+
working_dir: '~/default/path'
|
|
268
|
+
};
|
|
269
|
+
const item = {
|
|
270
|
+
repository: { full_name: 'unknown/repo' },
|
|
271
|
+
number: 456
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const actionConfig = buildActionConfigForItem(source, item);
|
|
275
|
+
|
|
276
|
+
// Should use source working_dir since repo not in config
|
|
277
|
+
assert.strictEqual(actionConfig.working_dir, '~/default/path');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
142
280
|
});
|
|
@@ -423,7 +423,7 @@ sources:
|
|
|
423
423
|
assert.deepStrictEqual(repos, ['myorg/backend']);
|
|
424
424
|
});
|
|
425
425
|
|
|
426
|
-
test('
|
|
426
|
+
test('repos array without repo template returns empty (needs item context)', async () => {
|
|
427
427
|
writeFileSync(configPath, `
|
|
428
428
|
sources:
|
|
429
429
|
- name: cross-repo
|
|
@@ -444,7 +444,8 @@ sources:
|
|
|
444
444
|
const item = { number: 123 };
|
|
445
445
|
const repos = resolveRepoForItem(source, item);
|
|
446
446
|
|
|
447
|
-
|
|
447
|
+
// Without a repo template, can't resolve from item - returns empty
|
|
448
|
+
assert.deepStrictEqual(repos, []);
|
|
448
449
|
});
|
|
449
450
|
|
|
450
451
|
test('returns empty array when no repo config', async () => {
|
|
@@ -614,6 +615,50 @@ sources:
|
|
|
614
615
|
|
|
615
616
|
assert.throws(() => getSources(), /Unknown preset: unknown\/preset/);
|
|
616
617
|
});
|
|
618
|
+
|
|
619
|
+
test('github presets include repo field for automatic resolution', async () => {
|
|
620
|
+
writeFileSync(configPath, `
|
|
621
|
+
sources:
|
|
622
|
+
- preset: github/my-issues
|
|
623
|
+
- preset: github/review-requests
|
|
624
|
+
- preset: github/my-prs-feedback
|
|
625
|
+
`);
|
|
626
|
+
|
|
627
|
+
const { loadRepoConfig, getSources, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
628
|
+
loadRepoConfig(configPath);
|
|
629
|
+
const sources = getSources();
|
|
630
|
+
|
|
631
|
+
// All GitHub presets should have repo field for automatic resolution
|
|
632
|
+
const mockItem = { repository: { full_name: 'myorg/backend' } };
|
|
633
|
+
|
|
634
|
+
for (const source of sources) {
|
|
635
|
+
assert.strictEqual(source.repo, '{repository.full_name}', `Preset ${source.name} should have repo field`);
|
|
636
|
+
const repos = resolveRepoForItem(source, mockItem);
|
|
637
|
+
assert.deepStrictEqual(repos, ['myorg/backend'], `Preset ${source.name} should resolve repo from item`);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test('source.repos acts as allowlist filter', async () => {
|
|
642
|
+
writeFileSync(configPath, `
|
|
643
|
+
sources:
|
|
644
|
+
- preset: github/my-issues
|
|
645
|
+
repos:
|
|
646
|
+
- myorg/backend
|
|
647
|
+
- myorg/frontend
|
|
648
|
+
`);
|
|
649
|
+
|
|
650
|
+
const { loadRepoConfig, getSources, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
651
|
+
loadRepoConfig(configPath);
|
|
652
|
+
const source = getSources()[0];
|
|
653
|
+
|
|
654
|
+
// Item from allowed repo should resolve
|
|
655
|
+
const allowedItem = { repository: { full_name: 'myorg/backend' } };
|
|
656
|
+
assert.deepStrictEqual(resolveRepoForItem(source, allowedItem), ['myorg/backend']);
|
|
657
|
+
|
|
658
|
+
// Item from non-allowed repo should return empty (filtered out)
|
|
659
|
+
const filteredItem = { repository: { full_name: 'other/repo' } };
|
|
660
|
+
assert.deepStrictEqual(resolveRepoForItem(source, filteredItem), []);
|
|
661
|
+
});
|
|
617
662
|
});
|
|
618
663
|
|
|
619
664
|
describe('shorthand syntax', () => {
|