opencode-pilot 0.1.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/.devcontainer/devcontainer.json +16 -0
- package/.github/workflows/ci.yml +67 -0
- package/.releaserc.cjs +28 -0
- package/AGENTS.md +71 -0
- package/CONTRIBUTING.md +102 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/bin/opencode-pilot +809 -0
- package/dist/opencode-ntfy.tar.gz +0 -0
- package/examples/config.yaml +73 -0
- package/examples/templates/default.md +7 -0
- package/examples/templates/devcontainer.md +7 -0
- package/examples/templates/review-feedback.md +7 -0
- package/examples/templates/review.md +15 -0
- package/install.sh +246 -0
- package/package.json +40 -0
- package/plugin/config.js +76 -0
- package/plugin/index.js +260 -0
- package/plugin/logger.js +125 -0
- package/plugin/notifier.js +110 -0
- package/service/actions.js +334 -0
- package/service/io.opencode.ntfy.plist +29 -0
- package/service/logger.js +82 -0
- package/service/poll-service.js +246 -0
- package/service/poller.js +339 -0
- package/service/readiness.js +234 -0
- package/service/repo-config.js +222 -0
- package/service/server.js +1523 -0
- package/service/utils.js +21 -0
- package/test/run_tests.bash +34 -0
- package/test/test_actions.bash +263 -0
- package/test/test_cli.bash +161 -0
- package/test/test_config.bash +438 -0
- package/test/test_helper.bash +140 -0
- package/test/test_logger.bash +401 -0
- package/test/test_notifier.bash +310 -0
- package/test/test_plist.bash +125 -0
- package/test/test_plugin.bash +952 -0
- package/test/test_poll_service.bash +179 -0
- package/test/test_poller.bash +120 -0
- package/test/test_readiness.bash +313 -0
- package/test/test_repo_config.bash +406 -0
- package/test/test_service.bash +1342 -0
- package/test/unit/actions.test.js +235 -0
- package/test/unit/config.test.js +86 -0
- package/test/unit/paths.test.js +77 -0
- package/test/unit/poll-service.test.js +142 -0
- package/test/unit/poller.test.js +347 -0
- package/test/unit/repo-config.test.js +441 -0
- package/test/unit/utils.test.js +53 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for actions.js - session creation with new config format
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir, homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
describe('actions.js', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
let templatesDir;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = mkdtempSync(join(tmpdir(), 'opencode-pilot-actions-test-'));
|
|
17
|
+
templatesDir = join(tempDir, 'templates');
|
|
18
|
+
mkdirSync(templatesDir);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('expandTemplate', () => {
|
|
26
|
+
test('expands simple placeholders', async () => {
|
|
27
|
+
const { expandTemplate } = await import('../../service/actions.js');
|
|
28
|
+
|
|
29
|
+
const result = expandTemplate('{title}\n\n{body}', {
|
|
30
|
+
title: 'Fix bug',
|
|
31
|
+
body: 'The bug is bad'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.strictEqual(result, 'Fix bug\n\nThe bug is bad');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('preserves unmatched placeholders', async () => {
|
|
38
|
+
const { expandTemplate } = await import('../../service/actions.js');
|
|
39
|
+
|
|
40
|
+
const result = expandTemplate('{title}\n\n{missing}', {
|
|
41
|
+
title: 'Fix bug'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.strictEqual(result, 'Fix bug\n\n{missing}');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('expands nested field references', async () => {
|
|
48
|
+
const { expandTemplate } = await import('../../service/actions.js');
|
|
49
|
+
|
|
50
|
+
const result = expandTemplate('Repo: {repository.full_name}', {
|
|
51
|
+
repository: { full_name: 'athal7/opencode-pilot' }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.strictEqual(result, 'Repo: athal7/opencode-pilot');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('preserves unmatched nested placeholders', async () => {
|
|
58
|
+
const { expandTemplate } = await import('../../service/actions.js');
|
|
59
|
+
|
|
60
|
+
const result = expandTemplate('{team.name}', {
|
|
61
|
+
team: {}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.strictEqual(result, '{team.name}');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('buildPromptFromTemplate', () => {
|
|
69
|
+
test('loads template from file and expands', async () => {
|
|
70
|
+
writeFileSync(join(templatesDir, 'default.md'), '{title}\n\n{body}');
|
|
71
|
+
|
|
72
|
+
const { buildPromptFromTemplate } = await import('../../service/actions.js');
|
|
73
|
+
|
|
74
|
+
const item = { title: 'Fix bug', body: 'Details here' };
|
|
75
|
+
const prompt = buildPromptFromTemplate('default', item, templatesDir);
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(prompt, 'Fix bug\n\nDetails here');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('returns fallback when template not found', async () => {
|
|
81
|
+
const { buildPromptFromTemplate } = await import('../../service/actions.js');
|
|
82
|
+
|
|
83
|
+
const item = { title: 'Fix bug', body: 'Details here' };
|
|
84
|
+
const prompt = buildPromptFromTemplate('nonexistent', item, templatesDir);
|
|
85
|
+
|
|
86
|
+
// Should fall back to title + body
|
|
87
|
+
assert.strictEqual(prompt, 'Fix bug\n\nDetails here');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('handles item with only title', async () => {
|
|
91
|
+
const { buildPromptFromTemplate } = await import('../../service/actions.js');
|
|
92
|
+
|
|
93
|
+
const item = { title: 'Fix bug' };
|
|
94
|
+
const prompt = buildPromptFromTemplate('nonexistent', item, templatesDir);
|
|
95
|
+
|
|
96
|
+
assert.strictEqual(prompt, 'Fix bug');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('getActionConfig', () => {
|
|
101
|
+
test('merges source, repo, and defaults', async () => {
|
|
102
|
+
const { getActionConfig } = await import('../../service/actions.js');
|
|
103
|
+
|
|
104
|
+
const source = {
|
|
105
|
+
name: 'my-issues',
|
|
106
|
+
prompt: 'custom',
|
|
107
|
+
agent: 'reviewer'
|
|
108
|
+
};
|
|
109
|
+
const repoConfig = {
|
|
110
|
+
path: '~/code/backend',
|
|
111
|
+
session: { name: 'issue-{number}' }
|
|
112
|
+
};
|
|
113
|
+
const defaults = {
|
|
114
|
+
prompt: 'default',
|
|
115
|
+
working_dir: '~'
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const config = getActionConfig(source, repoConfig, defaults);
|
|
119
|
+
|
|
120
|
+
// Source overrides
|
|
121
|
+
assert.strictEqual(config.prompt, 'custom');
|
|
122
|
+
assert.strictEqual(config.agent, 'reviewer');
|
|
123
|
+
// Repo config
|
|
124
|
+
assert.strictEqual(config.path, '~/code/backend');
|
|
125
|
+
assert.strictEqual(config.session.name, 'issue-{number}');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('falls back to defaults when no source/repo overrides', async () => {
|
|
129
|
+
const { getActionConfig } = await import('../../service/actions.js');
|
|
130
|
+
|
|
131
|
+
const source = { name: 'my-issues' };
|
|
132
|
+
const repoConfig = {};
|
|
133
|
+
const defaults = {
|
|
134
|
+
prompt: 'default',
|
|
135
|
+
working_dir: '~/scratch'
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const config = getActionConfig(source, repoConfig, defaults);
|
|
139
|
+
|
|
140
|
+
assert.strictEqual(config.prompt, 'default');
|
|
141
|
+
assert.strictEqual(config.working_dir, '~/scratch');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('source working_dir overrides repo path', async () => {
|
|
145
|
+
const { getActionConfig } = await import('../../service/actions.js');
|
|
146
|
+
|
|
147
|
+
const source = {
|
|
148
|
+
name: 'cross-repo',
|
|
149
|
+
working_dir: '~/workspaces'
|
|
150
|
+
};
|
|
151
|
+
const repoConfig = {
|
|
152
|
+
path: '~/code/backend'
|
|
153
|
+
};
|
|
154
|
+
const defaults = {};
|
|
155
|
+
|
|
156
|
+
const config = getActionConfig(source, repoConfig, defaults);
|
|
157
|
+
|
|
158
|
+
assert.strictEqual(config.working_dir, '~/workspaces');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('getCommandInfoNew', () => {
|
|
164
|
+
test('builds command with all options', async () => {
|
|
165
|
+
writeFileSync(join(templatesDir, 'default.md'), '{title}\n\n{body}');
|
|
166
|
+
|
|
167
|
+
const { getCommandInfoNew } = await import('../../service/actions.js');
|
|
168
|
+
|
|
169
|
+
const item = { number: 123, title: 'Fix bug', body: 'Details' };
|
|
170
|
+
const config = {
|
|
171
|
+
path: '~/code/backend',
|
|
172
|
+
prompt: 'default',
|
|
173
|
+
agent: 'coder',
|
|
174
|
+
session: { name: 'issue-{number}' }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const cmdInfo = getCommandInfoNew(item, config, templatesDir);
|
|
178
|
+
|
|
179
|
+
assert.strictEqual(cmdInfo.cwd, join(homedir(), 'code/backend'));
|
|
180
|
+
assert.ok(cmdInfo.args.includes('opencode'));
|
|
181
|
+
assert.ok(cmdInfo.args.includes('run'));
|
|
182
|
+
assert.ok(cmdInfo.args.includes('--title'));
|
|
183
|
+
assert.ok(cmdInfo.args.includes('issue-123'));
|
|
184
|
+
assert.ok(cmdInfo.args.includes('--agent'));
|
|
185
|
+
assert.ok(cmdInfo.args.includes('coder'));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('uses working_dir when no path', async () => {
|
|
189
|
+
const { getCommandInfoNew } = await import('../../service/actions.js');
|
|
190
|
+
|
|
191
|
+
const item = { id: 'reminder-1', title: 'Do something' };
|
|
192
|
+
const config = {
|
|
193
|
+
working_dir: '~/scratch',
|
|
194
|
+
prompt: 'default'
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const cmdInfo = getCommandInfoNew(item, config, templatesDir);
|
|
198
|
+
|
|
199
|
+
assert.strictEqual(cmdInfo.cwd, join(homedir(), 'scratch'));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('defaults to home dir when no path or working_dir', async () => {
|
|
203
|
+
const { getCommandInfoNew } = await import('../../service/actions.js');
|
|
204
|
+
|
|
205
|
+
const item = { title: 'Do something' };
|
|
206
|
+
const config = { prompt: 'default' };
|
|
207
|
+
|
|
208
|
+
const cmdInfo = getCommandInfoNew(item, config, templatesDir);
|
|
209
|
+
|
|
210
|
+
assert.strictEqual(cmdInfo.cwd, homedir());
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('includes prompt from template as message', async () => {
|
|
214
|
+
writeFileSync(join(templatesDir, 'devcontainer.md'), '/devcontainer issue-{number}\n\n{title}\n\n{body}');
|
|
215
|
+
|
|
216
|
+
const { getCommandInfoNew } = await import('../../service/actions.js');
|
|
217
|
+
|
|
218
|
+
const item = { number: 66, title: 'Fix bug', body: 'Details' };
|
|
219
|
+
const config = {
|
|
220
|
+
path: '~/code/backend',
|
|
221
|
+
prompt: 'devcontainer'
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const cmdInfo = getCommandInfoNew(item, config, templatesDir);
|
|
225
|
+
|
|
226
|
+
// Should NOT have --command flag (slash command is in template)
|
|
227
|
+
assert.ok(!cmdInfo.args.includes('--command'), 'Should not include --command flag');
|
|
228
|
+
|
|
229
|
+
// Prompt should include the /devcontainer command
|
|
230
|
+
const lastArg = cmdInfo.args[cmdInfo.args.length - 1];
|
|
231
|
+
assert.ok(lastArg.includes('/devcontainer issue-66'), 'Prompt should include /devcontainer command');
|
|
232
|
+
assert.ok(lastArg.includes('Fix bug'), 'Prompt should include title');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for config.js - unified YAML configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
|
|
11
|
+
// We'll test the module by creating temp config files
|
|
12
|
+
|
|
13
|
+
describe('config.js', () => {
|
|
14
|
+
let tempDir;
|
|
15
|
+
let configPath;
|
|
16
|
+
let templatesDir;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = mkdtempSync(join(tmpdir(), 'opencode-pilot-test-'));
|
|
20
|
+
configPath = join(tempDir, 'config.yaml');
|
|
21
|
+
templatesDir = join(tempDir, 'templates');
|
|
22
|
+
mkdirSync(templatesDir);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('loadConfig', () => {
|
|
30
|
+
test('reads notifications from config.yaml', async () => {
|
|
31
|
+
writeFileSync(configPath, `
|
|
32
|
+
notifications:
|
|
33
|
+
topic: test-topic
|
|
34
|
+
server: https://custom.ntfy.sh
|
|
35
|
+
idle_delay_ms: 600000
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
const { loadConfig } = await import('../../plugin/config.js');
|
|
39
|
+
const config = loadConfig(configPath);
|
|
40
|
+
|
|
41
|
+
assert.strictEqual(config.topic, 'test-topic');
|
|
42
|
+
assert.strictEqual(config.server, 'https://custom.ntfy.sh');
|
|
43
|
+
assert.strictEqual(config.idleDelayMs, 600000);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('returns defaults when config file missing', async () => {
|
|
47
|
+
const { loadConfig } = await import('../../plugin/config.js');
|
|
48
|
+
const config = loadConfig('/nonexistent/path/config.yaml');
|
|
49
|
+
|
|
50
|
+
assert.strictEqual(config.server, 'https://ntfy.sh');
|
|
51
|
+
assert.strictEqual(config.idleDelayMs, 300000);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('handles all config options', async () => {
|
|
55
|
+
writeFileSync(configPath, `
|
|
56
|
+
notifications:
|
|
57
|
+
topic: my-topic
|
|
58
|
+
server: https://ntfy.example.com
|
|
59
|
+
token: tk_xxx
|
|
60
|
+
idle_delay_ms: 600000
|
|
61
|
+
idle_notify: false
|
|
62
|
+
error_notify: false
|
|
63
|
+
error_debounce_ms: 120000
|
|
64
|
+
retry_notify_first: false
|
|
65
|
+
retry_notify_after: 5
|
|
66
|
+
debug: true
|
|
67
|
+
debug_path: /custom/debug.log
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
const { loadConfig } = await import('../../plugin/config.js');
|
|
71
|
+
const config = loadConfig(configPath);
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(config.topic, 'my-topic');
|
|
74
|
+
assert.strictEqual(config.server, 'https://ntfy.example.com');
|
|
75
|
+
assert.strictEqual(config.authToken, 'tk_xxx');
|
|
76
|
+
assert.strictEqual(config.idleDelayMs, 600000);
|
|
77
|
+
assert.strictEqual(config.idleNotify, false);
|
|
78
|
+
assert.strictEqual(config.errorNotify, false);
|
|
79
|
+
assert.strictEqual(config.errorDebounceMs, 120000);
|
|
80
|
+
assert.strictEqual(config.retryNotifyFirst, false);
|
|
81
|
+
assert.strictEqual(config.retryNotifyAfter, 5);
|
|
82
|
+
assert.strictEqual(config.debug, true);
|
|
83
|
+
assert.strictEqual(config.debugPath, '/custom/debug.log');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for consistent path naming across the codebase.
|
|
3
|
+
*
|
|
4
|
+
* These tests ensure all config/socket paths use "opencode-pilot"
|
|
5
|
+
* and not the old "opencode-ntfy" name.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { test, describe } from 'node:test';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const ROOT_DIR = join(__dirname, '..', '..');
|
|
17
|
+
const PLUGIN_DIR = join(ROOT_DIR, 'plugin');
|
|
18
|
+
const SERVICE_DIR = join(ROOT_DIR, 'service');
|
|
19
|
+
|
|
20
|
+
describe('Path naming consistency', () => {
|
|
21
|
+
|
|
22
|
+
describe('config.js', () => {
|
|
23
|
+
const configPath = join(PLUGIN_DIR, 'config.js');
|
|
24
|
+
const content = readFileSync(configPath, 'utf8');
|
|
25
|
+
|
|
26
|
+
test('uses opencode-pilot config path', () => {
|
|
27
|
+
assert.match(content, /opencode-pilot.*config\.yaml/,
|
|
28
|
+
'config.js should reference opencode-pilot config path');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('does not reference old opencode-ntfy path', () => {
|
|
32
|
+
assert.doesNotMatch(content, /opencode-ntfy/,
|
|
33
|
+
'config.js should not reference old opencode-ntfy name');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('logger.js', () => {
|
|
38
|
+
const loggerPath = join(PLUGIN_DIR, 'logger.js');
|
|
39
|
+
const content = readFileSync(loggerPath, 'utf8');
|
|
40
|
+
|
|
41
|
+
test('uses opencode-pilot debug log path', () => {
|
|
42
|
+
assert.match(content, /opencode-pilot.*debug\.log/,
|
|
43
|
+
'logger.js should reference opencode-pilot debug log path');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('does not reference old opencode-ntfy path', () => {
|
|
47
|
+
assert.doesNotMatch(content, /opencode-ntfy/,
|
|
48
|
+
'logger.js should not reference old opencode-ntfy name');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('server.js', () => {
|
|
53
|
+
const serverPath = join(SERVICE_DIR, 'server.js');
|
|
54
|
+
const content = readFileSync(serverPath, 'utf8');
|
|
55
|
+
|
|
56
|
+
test('uses opencode-pilot socket path', () => {
|
|
57
|
+
assert.match(content, /opencode-pilot\.sock/,
|
|
58
|
+
'server.js should reference opencode-pilot socket path');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('uses opencode-pilot config path', () => {
|
|
62
|
+
assert.match(content, /opencode-pilot.*config\.json|opencode-pilot/,
|
|
63
|
+
'server.js should reference opencode-pilot paths');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('index.js (plugin entry)', () => {
|
|
68
|
+
const indexPath = join(PLUGIN_DIR, 'index.js');
|
|
69
|
+
const content = readFileSync(indexPath, 'utf8');
|
|
70
|
+
|
|
71
|
+
test('does not reference old opencode-ntfy name in comments', () => {
|
|
72
|
+
// Allow "ntfy" alone (the service name) but not "opencode-ntfy"
|
|
73
|
+
assert.doesNotMatch(content, /opencode-ntfy/,
|
|
74
|
+
'index.js should not reference old opencode-ntfy name');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for poll-service.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, describe, beforeEach, afterEach, mock } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
|
|
11
|
+
describe('poll-service.js', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
let configPath;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = mkdtempSync(join(tmpdir(), 'opencode-pilot-poll-service-test-'));
|
|
17
|
+
configPath = join(tempDir, 'config.yaml');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('source configuration', () => {
|
|
25
|
+
test('parses sources with tool.mcp and tool.name', async () => {
|
|
26
|
+
const config = `
|
|
27
|
+
sources:
|
|
28
|
+
- name: my-issues
|
|
29
|
+
tool:
|
|
30
|
+
mcp: github
|
|
31
|
+
name: search_issues
|
|
32
|
+
args:
|
|
33
|
+
q: "is:issue assignee:@me"
|
|
34
|
+
item:
|
|
35
|
+
id: "{html_url}"
|
|
36
|
+
`;
|
|
37
|
+
writeFileSync(configPath, config);
|
|
38
|
+
|
|
39
|
+
const { loadRepoConfig, getAllSources } = await import('../../service/repo-config.js');
|
|
40
|
+
loadRepoConfig(configPath);
|
|
41
|
+
const sources = getAllSources();
|
|
42
|
+
|
|
43
|
+
assert.strictEqual(sources.length, 1);
|
|
44
|
+
assert.strictEqual(sources[0].name, 'my-issues');
|
|
45
|
+
assert.ok(sources[0].tool, 'Source should have tool config');
|
|
46
|
+
assert.strictEqual(sources[0].tool.mcp, 'github');
|
|
47
|
+
assert.strictEqual(sources[0].tool.name, 'search_issues');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('hasToolConfig validates source configuration', async () => {
|
|
51
|
+
const { hasToolConfig } = await import('../../service/poll-service.js');
|
|
52
|
+
|
|
53
|
+
// Valid config
|
|
54
|
+
const valid = {
|
|
55
|
+
name: 'test',
|
|
56
|
+
tool: { mcp: 'github', name: 'search_issues' },
|
|
57
|
+
args: {}
|
|
58
|
+
};
|
|
59
|
+
assert.strictEqual(hasToolConfig(valid), true);
|
|
60
|
+
|
|
61
|
+
// Missing tool
|
|
62
|
+
const missingTool = { name: 'test' };
|
|
63
|
+
assert.strictEqual(hasToolConfig(missingTool), false);
|
|
64
|
+
|
|
65
|
+
// Missing mcp
|
|
66
|
+
const missingMcp = { name: 'test', tool: { name: 'search_issues' } };
|
|
67
|
+
assert.strictEqual(hasToolConfig(missingMcp), false);
|
|
68
|
+
|
|
69
|
+
// Missing tool.name
|
|
70
|
+
const missingName = { name: 'test', tool: { mcp: 'github' } };
|
|
71
|
+
assert.strictEqual(hasToolConfig(missingName), false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('buildActionConfigFromSource', () => {
|
|
76
|
+
test('includes source-level agent, model, prompt, and working_dir', async () => {
|
|
77
|
+
const { buildActionConfigFromSource } = await import('../../service/poll-service.js');
|
|
78
|
+
|
|
79
|
+
const source = {
|
|
80
|
+
name: 'test-source',
|
|
81
|
+
agent: 'plan',
|
|
82
|
+
model: 'claude-opus',
|
|
83
|
+
prompt: 'devcontainer',
|
|
84
|
+
working_dir: '~/code/project',
|
|
85
|
+
session: { name: 'issue-{number}' }
|
|
86
|
+
};
|
|
87
|
+
const repoConfig = {
|
|
88
|
+
path: '~/code/default',
|
|
89
|
+
prompt: 'default'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const config = buildActionConfigFromSource(source, repoConfig);
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(config.agent, 'plan');
|
|
95
|
+
assert.strictEqual(config.model, 'claude-opus');
|
|
96
|
+
assert.strictEqual(config.prompt, 'devcontainer');
|
|
97
|
+
assert.strictEqual(config.working_dir, '~/code/project');
|
|
98
|
+
assert.deepStrictEqual(config.session, { name: 'issue-{number}' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('falls back to repoConfig when source fields missing', async () => {
|
|
102
|
+
const { buildActionConfigFromSource } = await import('../../service/poll-service.js');
|
|
103
|
+
|
|
104
|
+
const source = {
|
|
105
|
+
name: 'test-source'
|
|
106
|
+
// No agent, model, prompt, working_dir
|
|
107
|
+
};
|
|
108
|
+
const repoConfig = {
|
|
109
|
+
path: '~/code/default',
|
|
110
|
+
prompt: 'default',
|
|
111
|
+
session: { name: 'default-{number}' }
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const config = buildActionConfigFromSource(source, repoConfig);
|
|
115
|
+
|
|
116
|
+
assert.strictEqual(config.prompt, 'default');
|
|
117
|
+
assert.strictEqual(config.repo_path, '~/code/default');
|
|
118
|
+
assert.deepStrictEqual(config.session, { name: 'default-{number}' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('source fields override repoConfig fields', async () => {
|
|
122
|
+
const { buildActionConfigFromSource } = await import('../../service/poll-service.js');
|
|
123
|
+
|
|
124
|
+
const source = {
|
|
125
|
+
name: 'test-source',
|
|
126
|
+
prompt: 'review',
|
|
127
|
+
agent: 'code'
|
|
128
|
+
};
|
|
129
|
+
const repoConfig = {
|
|
130
|
+
path: '~/code/default',
|
|
131
|
+
prompt: 'default',
|
|
132
|
+
agent: 'plan' // Should be overridden
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const config = buildActionConfigFromSource(source, repoConfig);
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(config.prompt, 'review');
|
|
138
|
+
assert.strictEqual(config.agent, 'code');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
});
|
|
142
|
+
});
|