opencode-pilot 0.10.0 → 0.11.1

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/README.md CHANGED
@@ -94,6 +94,18 @@ opencode-pilot test-mapping MCP # Test field mappings
94
94
 
95
95
  ## Known Issues
96
96
 
97
+ ### Sessions attached to global server run in wrong directory
98
+
99
+ When using `server_port` to attach sessions to a global OpenCode server (e.g., OpenCode Desktop with worktree="/"), sessions are created in the server's working directory (typically home) instead of the project directory. This means:
100
+
101
+ - File tools resolve paths relative to home, not the project
102
+ - The agent sees the wrong `Working directory` in system prompt
103
+ - Git operations may target the wrong repository
104
+
105
+ **Workaround**: Don't set `server_port` in your config. Sessions will run in the correct directory but won't appear in OpenCode Desktop.
106
+
107
+ **Upstream issue**: [anomalyco/opencode#7376](https://github.com/anomalyco/opencode/issues/7376)
108
+
97
109
  ### Working directory doesn't switch when templates create worktrees/devcontainers
98
110
 
99
111
  When a template instructs the agent to create a git worktree or switch to a devcontainer, OpenCode's internal working directory context (`Instance.directory`) doesn't update. This means:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { loadRepoConfig, getRepoConfig, getAllSources, getToolProviderConfig, resolveRepoForItem, getCleanupTtlDays } from "./repo-config.js";
13
- import { createPoller, pollGenericSource } from "./poller.js";
13
+ import { createPoller, pollGenericSource, enrichItemsWithComments } from "./poller.js";
14
14
  import { evaluateReadiness, sortByPriority } from "./readiness.js";
15
15
  import { executeAction, buildCommand } from "./actions.js";
16
16
  import { debug } from "./logger.js";
@@ -126,6 +126,12 @@ export async function pollOnce(options = {}) {
126
126
  toolProviderConfig = getToolProviderConfig(source.tool.mcp);
127
127
  items = await pollGenericSource(source, { toolProviderConfig });
128
128
  debug(`Fetched ${items.length} items from ${sourceName}`);
129
+
130
+ // Enrich items with comments for bot filtering if configured
131
+ if (source.filter_bot_comments) {
132
+ items = await enrichItemsWithComments(items, source);
133
+ debug(`Enriched ${items.length} items with comments for bot filtering`);
134
+ }
129
135
  } catch (err) {
130
136
  console.error(`[poll] Error fetching from ${sourceName}: ${err.message}`);
131
137
  continue;
package/service/poller.js CHANGED
@@ -273,6 +273,163 @@ export async function pollGenericSource(source, options = {}) {
273
273
  }
274
274
  }
275
275
 
276
+ /**
277
+ * Fetch issue comments using gh CLI
278
+ *
279
+ * The GitHub MCP server doesn't have a tool to list issue comments,
280
+ * so we use gh CLI directly. This fetches the conversation thread
281
+ * where bots like Linear post their comments.
282
+ *
283
+ * @param {string} owner - Repository owner
284
+ * @param {string} repo - Repository name
285
+ * @param {number} number - Issue/PR number
286
+ * @param {number} timeout - Timeout in ms
287
+ * @returns {Promise<Array>} Array of comment objects
288
+ */
289
+ async function fetchIssueCommentsViaCli(owner, repo, number, timeout) {
290
+ const { exec } = await import('child_process');
291
+ const { promisify } = await import('util');
292
+ const execAsync = promisify(exec);
293
+
294
+ try {
295
+ const { stdout } = await Promise.race([
296
+ execAsync(`gh api repos/${owner}/${repo}/issues/${number}/comments`),
297
+ createTimeout(timeout, "gh api call"),
298
+ ]);
299
+
300
+ const comments = JSON.parse(stdout);
301
+ return Array.isArray(comments) ? comments : [];
302
+ } catch (err) {
303
+ // gh CLI might not be available or authenticated
304
+ console.error(`[poller] Error fetching issue comments via gh: ${err.message}`);
305
+ return [];
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Fetch comments for a GitHub issue/PR and enrich the item
311
+ *
312
+ * Fetches BOTH types of comments:
313
+ * 1. PR review comments (inline code comments) via MCP github_get_pull_request_comments
314
+ * 2. Issue comments (conversation thread) via gh CLI
315
+ *
316
+ * This is necessary because bots like Linear post to issue comments, not PR review comments.
317
+ *
318
+ * @param {object} item - Item with owner, repo_short, and number fields
319
+ * @param {object} [options] - Options
320
+ * @param {number} [options.timeout] - Timeout in ms (default: 30000)
321
+ * @param {string} [options.opencodeConfigPath] - Path to opencode.json for MCP config
322
+ * @returns {Promise<Array>} Array of comment objects (merged from both endpoints)
323
+ */
324
+ export async function fetchGitHubComments(item, options = {}) {
325
+ const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
326
+
327
+ // Extract owner and repo from item
328
+ // The item should have repository_full_name (e.g., "owner/repo") from mapping
329
+ const fullName = item.repository_full_name;
330
+ if (!fullName) {
331
+ console.error("[poller] Cannot fetch comments: missing repository_full_name");
332
+ return [];
333
+ }
334
+
335
+ const [owner, repo] = fullName.split("/");
336
+ const number = item.number;
337
+
338
+ if (!owner || !repo || !number) {
339
+ console.error("[poller] Cannot fetch comments: missing owner, repo, or number");
340
+ return [];
341
+ }
342
+
343
+ let mcpConfig;
344
+ try {
345
+ mcpConfig = getMcpConfig("github", options.opencodeConfigPath);
346
+ } catch {
347
+ console.error("[poller] GitHub MCP not configured, cannot fetch comments");
348
+ return [];
349
+ }
350
+
351
+ const client = new Client({ name: "opencode-pilot", version: "1.0.0" });
352
+
353
+ try {
354
+ const transport = await createTransport(mcpConfig);
355
+
356
+ await Promise.race([
357
+ client.connect(transport),
358
+ createTimeout(timeout, "MCP connection for comments"),
359
+ ]);
360
+
361
+ // Fetch both PR review comments (via MCP) AND issue comments (via gh CLI) in parallel
362
+ const [prCommentsResult, issueComments] = await Promise.all([
363
+ // PR review comments via MCP (may not be available on all MCP servers)
364
+ client.callTool({
365
+ name: "github_get_pull_request_comments",
366
+ arguments: { owner, repo, pull_number: number }
367
+ }).catch(() => null), // Gracefully handle if tool doesn't exist
368
+ // Issue comments via gh CLI (conversation thread where Linear bot posts)
369
+ fetchIssueCommentsViaCli(owner, repo, number, timeout),
370
+ ]);
371
+
372
+ // Parse PR review comments
373
+ let prComments = [];
374
+ const prText = prCommentsResult?.content?.[0]?.text;
375
+ if (prText) {
376
+ try {
377
+ const parsed = JSON.parse(prText);
378
+ prComments = Array.isArray(parsed) ? parsed : [];
379
+ } catch {
380
+ // Ignore parse errors
381
+ }
382
+ }
383
+
384
+ // Return merged comments from both sources
385
+ return [...prComments, ...issueComments];
386
+ } catch (err) {
387
+ console.error(`[poller] Error fetching comments: ${err.message}`);
388
+ return [];
389
+ } finally {
390
+ try {
391
+ await Promise.race([
392
+ client.close(),
393
+ new Promise(resolve => setTimeout(resolve, 3000)),
394
+ ]);
395
+ } catch {
396
+ // Ignore close errors
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Enrich items with comments for bot filtering
403
+ *
404
+ * For items from sources with filter_bot_comments: true, fetches comments
405
+ * and attaches them as _comments field for readiness evaluation.
406
+ *
407
+ * @param {Array} items - Items to enrich
408
+ * @param {object} source - Source configuration with optional filter_bot_comments
409
+ * @param {object} [options] - Options passed to fetchGitHubComments
410
+ * @returns {Promise<Array>} Items with _comments field added
411
+ */
412
+ export async function enrichItemsWithComments(items, source, options = {}) {
413
+ // Skip if not configured or not a GitHub source
414
+ if (!source.filter_bot_comments || source.tool?.mcp !== "github") {
415
+ return items;
416
+ }
417
+
418
+ // Fetch comments for each item (could be parallelized with Promise.all for speed)
419
+ const enrichedItems = [];
420
+ for (const item of items) {
421
+ // Only fetch if item has comments
422
+ if (item.comments > 0) {
423
+ const comments = await fetchGitHubComments(item, options);
424
+ enrichedItems.push({ ...item, _comments: comments });
425
+ } else {
426
+ enrichedItems.push(item);
427
+ }
428
+ }
429
+
430
+ return enrichedItems;
431
+ }
432
+
276
433
  /**
277
434
  * Create a poller instance with state tracking
278
435
  *
@@ -57,3 +57,6 @@ my-prs-feedback:
57
57
  reprocess_on:
58
58
  - state
59
59
  - updated_at
60
+ # Filter out PRs where all comments are from bots or the PR author
61
+ # Fetches comments via API and enriches items with _comments for readiness check
62
+ filter_bot_comments: true
@@ -4,9 +4,12 @@
4
4
  * Evaluates whether an issue is ready to be worked on based on:
5
5
  * - Label constraints (blocking labels, required labels)
6
6
  * - Dependencies (blocked by references in body)
7
+ * - Bot comment filtering (for PR feedback sources)
7
8
  * - Priority scoring (label weights, age bonus)
8
9
  */
9
10
 
11
+ import { hasNonBotFeedback } from "./utils.js";
12
+
10
13
  /**
11
14
  * Dependency reference patterns in issue body
12
15
  */
@@ -131,6 +134,45 @@ export function checkDependencies(issue, config) {
131
134
  return { ready: true };
132
135
  }
133
136
 
137
+ /**
138
+ * Check if a PR/issue has meaningful (non-bot, non-author) comments
139
+ *
140
+ * This check is only applied when the item has been enriched with `_comments`
141
+ * (an array of comment objects with user.login and user.type fields).
142
+ * Items without `_comments` are considered ready (check is skipped).
143
+ *
144
+ * Used to filter PRs from feedback sources where bot comments (CI, coverage, etc.)
145
+ * should not trigger the author to take action.
146
+ *
147
+ * @param {object} item - Item with optional _comments array and user.login
148
+ * @param {object} config - Repo config (currently unused but kept for API consistency)
149
+ * @returns {object} { ready: boolean, reason?: string }
150
+ */
151
+ export function checkBotComments(item, config) {
152
+ // Skip check if no _comments field (item not enriched)
153
+ if (!item._comments) {
154
+ return { ready: true };
155
+ }
156
+
157
+ // Empty comments array means no comments - consider ready (no feedback)
158
+ if (item._comments.length === 0) {
159
+ return { ready: true };
160
+ }
161
+
162
+ // Get author username
163
+ const authorUsername = item.user?.login;
164
+
165
+ // Check if there's non-bot, non-author feedback
166
+ if (hasNonBotFeedback(item._comments, authorUsername)) {
167
+ return { ready: true };
168
+ }
169
+
170
+ return {
171
+ ready: false,
172
+ reason: "Only bot or author comments - no human feedback requiring action",
173
+ };
174
+ }
175
+
134
176
  /**
135
177
  * Calculate priority score for an issue
136
178
  * @param {object} issue - Issue with labels and created_at
@@ -194,6 +236,16 @@ export function evaluateReadiness(issue, config) {
194
236
  };
195
237
  }
196
238
 
239
+ // Check bot comments (for PRs enriched with _comments)
240
+ const botResult = checkBotComments(issue, config);
241
+ if (!botResult.ready) {
242
+ return {
243
+ ready: false,
244
+ reason: botResult.reason,
245
+ priority: 0,
246
+ };
247
+ }
248
+
197
249
  // Calculate priority for ready issues
198
250
  const priority = calculatePriority(issue, config);
199
251
 
package/service/utils.js CHANGED
@@ -19,3 +19,65 @@ export function getNestedValue(obj, path) {
19
19
  }
20
20
  return value;
21
21
  }
22
+
23
+ /**
24
+ * Check if a username represents a bot account
25
+ *
26
+ * Detects bots by:
27
+ * 1. Username suffix: [bot] (e.g., "github-actions[bot]", "dependabot[bot]")
28
+ * 2. User type field: "Bot" (GitHub API provides this)
29
+ *
30
+ * @param {string} username - GitHub username to check
31
+ * @param {string} [type] - User type from API (e.g., "Bot", "User")
32
+ * @returns {boolean} True if the user is a bot
33
+ */
34
+ export function isBot(username, type) {
35
+ // Handle null/undefined/empty
36
+ if (!username) return false;
37
+
38
+ // Check user type field (GitHub API provides "Bot" type)
39
+ if (type && type.toLowerCase() === "bot") return true;
40
+
41
+ // Check for [bot] suffix in username
42
+ if (username.toLowerCase().endsWith("[bot]")) return true;
43
+
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Check if a PR/issue has non-bot feedback (comments from humans other than the author)
49
+ *
50
+ * Used to filter out PRs where only bots have commented, since those don't
51
+ * require the author's attention for human feedback.
52
+ *
53
+ * @param {Array} comments - Array of comment objects with user.login and user.type
54
+ * @param {string} authorUsername - Username of the PR/issue author
55
+ * @returns {boolean} True if there's at least one non-bot, non-author comment
56
+ */
57
+ export function hasNonBotFeedback(comments, authorUsername) {
58
+ // Handle null/undefined/empty
59
+ if (!comments || !Array.isArray(comments) || comments.length === 0) {
60
+ return false;
61
+ }
62
+
63
+ const authorLower = authorUsername?.toLowerCase();
64
+
65
+ for (const comment of comments) {
66
+ const user = comment.user;
67
+ if (!user) continue;
68
+
69
+ const username = user.login;
70
+ const userType = user.type;
71
+
72
+ // Skip if it's a bot
73
+ if (isBot(username, userType)) continue;
74
+
75
+ // Skip if it's the author themselves
76
+ if (authorLower && username?.toLowerCase() === authorLower) continue;
77
+
78
+ // Found a non-bot, non-author comment
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ }
@@ -734,6 +734,29 @@ describe('poller.js', () => {
734
734
  });
735
735
  });
736
736
 
737
+ describe('fetchGitHubComments', () => {
738
+ // Note: These tests document the expected behavior
739
+ // Actual MCP calls require mocking which is complex
740
+
741
+ test('should fetch both PR review comments AND issue comments', async () => {
742
+ // The issue: Linear bot posts to issue comments endpoint, not PR review comments
743
+ // PR review comments: GET /repos/{owner}/{repo}/pulls/{pull_number}/comments
744
+ // Issue comments: GET /repos/{owner}/{repo}/issues/{issue_number}/comments
745
+ //
746
+ // For proper bot filtering, we need to check BOTH endpoints
747
+ // Currently only PR review comments are fetched, causing Linear bot
748
+ // comments to be missed (they appear in issue comments)
749
+
750
+ // This test documents the expected behavior - fetchGitHubComments should
751
+ // return comments from both endpoints merged together
752
+ const { fetchGitHubComments } = await import('../../service/poller.js');
753
+
754
+ // Without proper mocking, we can only test the function exists
755
+ // and accepts the right parameters
756
+ assert.strictEqual(typeof fetchGitHubComments, 'function');
757
+ });
758
+ });
759
+
737
760
  describe('parseJsonArray', () => {
738
761
  test('parses direct array response', async () => {
739
762
  const { parseJsonArray } = await import('../../service/poller.js');
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Tests for readiness.js - Issue/PR readiness evaluation
3
+ */
4
+
5
+ import { test, describe, beforeEach, afterEach } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import { mkdtempSync, rmSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { tmpdir } from 'os';
10
+
11
+ describe('readiness.js', () => {
12
+ let tempDir;
13
+
14
+ beforeEach(() => {
15
+ tempDir = mkdtempSync(join(tmpdir(), 'opencode-pilot-readiness-test-'));
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(tempDir, { recursive: true, force: true });
20
+ });
21
+
22
+ describe('checkLabels', () => {
23
+ test('returns ready when no label constraints', async () => {
24
+ const { checkLabels } = await import('../../service/readiness.js');
25
+
26
+ const issue = { labels: ['bug', 'help wanted'] };
27
+ const config = {};
28
+
29
+ const result = checkLabels(issue, config);
30
+
31
+ assert.strictEqual(result.ready, true);
32
+ });
33
+
34
+ test('returns not ready when has blocking label', async () => {
35
+ const { checkLabels } = await import('../../service/readiness.js');
36
+
37
+ const issue = { labels: ['bug', 'wip'] };
38
+ const config = {
39
+ readiness: {
40
+ labels: {
41
+ exclude: ['wip', 'do-not-merge']
42
+ }
43
+ }
44
+ };
45
+
46
+ const result = checkLabels(issue, config);
47
+
48
+ assert.strictEqual(result.ready, false);
49
+ assert.ok(result.reason.includes('wip'));
50
+ });
51
+
52
+ test('returns not ready when missing required label', async () => {
53
+ const { checkLabels } = await import('../../service/readiness.js');
54
+
55
+ const issue = { labels: ['bug'] };
56
+ const config = {
57
+ readiness: {
58
+ labels: {
59
+ required: ['approved', 'ready']
60
+ }
61
+ }
62
+ };
63
+
64
+ const result = checkLabels(issue, config);
65
+
66
+ assert.strictEqual(result.ready, false);
67
+ assert.ok(result.reason.includes('approved'));
68
+ });
69
+
70
+ test('handles label objects with name property', async () => {
71
+ const { checkLabels } = await import('../../service/readiness.js');
72
+
73
+ const issue = {
74
+ labels: [
75
+ { name: 'bug' },
76
+ { name: 'approved' }
77
+ ]
78
+ };
79
+ const config = {
80
+ readiness: {
81
+ labels: {
82
+ required: ['approved']
83
+ }
84
+ }
85
+ };
86
+
87
+ const result = checkLabels(issue, config);
88
+
89
+ assert.strictEqual(result.ready, true);
90
+ });
91
+ });
92
+
93
+ describe('checkBotComments', () => {
94
+ test('returns ready when there are non-bot comments', async () => {
95
+ const { checkBotComments } = await import('../../service/readiness.js');
96
+
97
+ const pr = {
98
+ user: { login: 'author' },
99
+ _comments: [
100
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
101
+ { user: { login: 'reviewer', type: 'User' }, body: 'Please fix the bug' },
102
+ ]
103
+ };
104
+ const config = {};
105
+
106
+ const result = checkBotComments(pr, config);
107
+
108
+ assert.strictEqual(result.ready, true);
109
+ });
110
+
111
+ test('returns not ready when all comments are from bots', async () => {
112
+ const { checkBotComments } = await import('../../service/readiness.js');
113
+
114
+ const pr = {
115
+ user: { login: 'author' },
116
+ _comments: [
117
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
118
+ { user: { login: 'codecov[bot]', type: 'Bot' }, body: 'Coverage report' },
119
+ ]
120
+ };
121
+ const config = {};
122
+
123
+ const result = checkBotComments(pr, config);
124
+
125
+ assert.strictEqual(result.ready, false);
126
+ assert.ok(result.reason.includes('bot'));
127
+ });
128
+
129
+ test('returns not ready when only author and bots have commented', async () => {
130
+ const { checkBotComments } = await import('../../service/readiness.js');
131
+
132
+ const pr = {
133
+ user: { login: 'athal7' },
134
+ _comments: [
135
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
136
+ { user: { login: 'athal7', type: 'User' }, body: 'Added screenshots' },
137
+ ]
138
+ };
139
+ const config = {};
140
+
141
+ const result = checkBotComments(pr, config);
142
+
143
+ assert.strictEqual(result.ready, false);
144
+ });
145
+
146
+ test('returns ready when no _comments field (skip check)', async () => {
147
+ const { checkBotComments } = await import('../../service/readiness.js');
148
+
149
+ const pr = {
150
+ user: { login: 'author' },
151
+ comments: 5 // count only, no _comments enrichment
152
+ };
153
+ const config = {};
154
+
155
+ const result = checkBotComments(pr, config);
156
+
157
+ assert.strictEqual(result.ready, true);
158
+ });
159
+
160
+ test('returns ready when _comments is empty (no comments yet)', async () => {
161
+ const { checkBotComments } = await import('../../service/readiness.js');
162
+
163
+ const pr = {
164
+ user: { login: 'author' },
165
+ _comments: []
166
+ };
167
+ const config = {};
168
+
169
+ const result = checkBotComments(pr, config);
170
+
171
+ assert.strictEqual(result.ready, true);
172
+ });
173
+ });
174
+
175
+ describe('evaluateReadiness', () => {
176
+ test('checks bot comments when _comments is present', async () => {
177
+ const { evaluateReadiness } = await import('../../service/readiness.js');
178
+
179
+ const pr = {
180
+ user: { login: 'author' },
181
+ _comments: [
182
+ { user: { login: 'dependabot[bot]', type: 'Bot' }, body: 'Bump dependency' },
183
+ ]
184
+ };
185
+ const config = {};
186
+
187
+ const result = evaluateReadiness(pr, config);
188
+
189
+ assert.strictEqual(result.ready, false);
190
+ assert.ok(result.reason.includes('bot'));
191
+ });
192
+
193
+ test('passes when there is real human feedback', async () => {
194
+ const { evaluateReadiness } = await import('../../service/readiness.js');
195
+
196
+ const pr = {
197
+ user: { login: 'author' },
198
+ _comments: [
199
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
200
+ { user: { login: 'teammate', type: 'User' }, body: 'LGTM!' },
201
+ ]
202
+ };
203
+ const config = {};
204
+
205
+ const result = evaluateReadiness(pr, config);
206
+
207
+ assert.strictEqual(result.ready, true);
208
+ });
209
+ });
210
+ });
@@ -6,6 +6,102 @@ import { test, describe } from 'node:test';
6
6
  import assert from 'node:assert';
7
7
 
8
8
  describe('utils.js', () => {
9
+ describe('isBot', () => {
10
+ test('detects GitHub bot usernames with [bot] suffix', async () => {
11
+ const { isBot } = await import('../../service/utils.js');
12
+
13
+ assert.strictEqual(isBot('github-actions[bot]'), true);
14
+ assert.strictEqual(isBot('dependabot[bot]'), true);
15
+ assert.strictEqual(isBot('renovate[bot]'), true);
16
+ assert.strictEqual(isBot('codecov[bot]'), true);
17
+ });
18
+
19
+ test('detects bots by type field', async () => {
20
+ const { isBot } = await import('../../service/utils.js');
21
+
22
+ // When user object has type: "Bot"
23
+ assert.strictEqual(isBot('some-user', 'Bot'), true);
24
+ assert.strictEqual(isBot('another-user', 'bot'), true);
25
+ });
26
+
27
+ test('returns false for regular users', async () => {
28
+ const { isBot } = await import('../../service/utils.js');
29
+
30
+ assert.strictEqual(isBot('athal7'), false);
31
+ assert.strictEqual(isBot('octocat'), false);
32
+ assert.strictEqual(isBot('some-developer'), false);
33
+ });
34
+
35
+ test('returns false for regular users with type User', async () => {
36
+ const { isBot } = await import('../../service/utils.js');
37
+
38
+ assert.strictEqual(isBot('athal7', 'User'), false);
39
+ assert.strictEqual(isBot('octocat', 'user'), false);
40
+ });
41
+
42
+ test('handles edge cases', async () => {
43
+ const { isBot } = await import('../../service/utils.js');
44
+
45
+ assert.strictEqual(isBot(''), false);
46
+ assert.strictEqual(isBot(null), false);
47
+ assert.strictEqual(isBot(undefined), false);
48
+ });
49
+ });
50
+
51
+ describe('hasNonBotFeedback', () => {
52
+ test('returns true when there are non-bot, non-author comments', async () => {
53
+ const { hasNonBotFeedback } = await import('../../service/utils.js');
54
+
55
+ const comments = [
56
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
57
+ { user: { login: 'reviewer', type: 'User' }, body: 'Please fix the bug' },
58
+ ];
59
+
60
+ assert.strictEqual(hasNonBotFeedback(comments, 'athal7'), true);
61
+ });
62
+
63
+ test('returns false when all comments are from bots', async () => {
64
+ const { hasNonBotFeedback } = await import('../../service/utils.js');
65
+
66
+ const comments = [
67
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
68
+ { user: { login: 'codecov[bot]', type: 'Bot' }, body: 'Coverage report' },
69
+ ];
70
+
71
+ assert.strictEqual(hasNonBotFeedback(comments, 'athal7'), false);
72
+ });
73
+
74
+ test('returns false when only author has commented', async () => {
75
+ const { hasNonBotFeedback } = await import('../../service/utils.js');
76
+
77
+ const comments = [
78
+ { user: { login: 'github-actions[bot]', type: 'Bot' }, body: 'CI passed' },
79
+ { user: { login: 'athal7', type: 'User' }, body: 'Added screenshots' },
80
+ ];
81
+
82
+ assert.strictEqual(hasNonBotFeedback(comments, 'athal7'), false);
83
+ });
84
+
85
+ test('returns false for empty comments array', async () => {
86
+ const { hasNonBotFeedback } = await import('../../service/utils.js');
87
+
88
+ assert.strictEqual(hasNonBotFeedback([], 'athal7'), false);
89
+ assert.strictEqual(hasNonBotFeedback(null, 'athal7'), false);
90
+ assert.strictEqual(hasNonBotFeedback(undefined, 'athal7'), false);
91
+ });
92
+
93
+ test('handles nested user object with login and type', async () => {
94
+ const { hasNonBotFeedback } = await import('../../service/utils.js');
95
+
96
+ const comments = [
97
+ { user: { login: 'dependabot[bot]', type: 'Bot' }, body: 'Bump lodash' },
98
+ { user: { login: 'maintainer', type: 'User' }, body: 'LGTM' },
99
+ ];
100
+
101
+ assert.strictEqual(hasNonBotFeedback(comments, 'contributor'), true);
102
+ });
103
+ });
104
+
9
105
  describe('getNestedValue', () => {
10
106
  test('gets top-level value', async () => {
11
107
  const { getNestedValue } = await import('../../service/utils.js');