fix(issue-assistant): poll only open issues

This commit is contained in:
insleker 2026-04-05 18:30:20 +08:00
parent c00b30306c
commit dd56ab4554
3 changed files with 180 additions and 123 deletions

View File

@ -22,6 +22,7 @@ class GitHubClient {
final queryParts = <String>[ final queryParts = <String>[
for (final repo in repoList) 'repo:${repo.fullName}', for (final repo in repoList) 'repo:${repo.fullName}',
'is:issue', 'is:issue',
'state:open',
if (since != null) 'updated:>=${since.toUtc().toIso8601String()}', if (since != null) 'updated:>=${since.toUtc().toIso8601String()}',
]; ];

View File

@ -1261,16 +1261,19 @@ projects:
}); });
/// ```gherkin /// ```gherkin
/// Scenario: Aggregate one search request across multiple enabled projects /// Scenario: Aggregate one search request across multiple enabled projects while ignoring closed issues
/// Given two temporary project checkouts that can be used as local repo paths /// Given two temporary project checkouts that can be used as local repo paths
/// And GitHub issue data containing an explicit assistant mention in each repository /// And GitHub issue data containing an explicit assistant mention in each repository
/// And one of the search results is a closed issue
/// And a responder that emits a planning reply /// And a responder that emits a planning reply
/// And an orchestrator-style config with both repositories enabled /// And an orchestrator-style config with both repositories enabled
/// When the app processes one polling cycle /// When the app processes one polling cycle
/// Then it fetches issue updates through one aggregate GitHub search request /// Then it fetches issue updates through one aggregate GitHub search request for open issues only
/// And it still posts one reply in each repository /// And it still posts one reply in each repository
/// ``` /// ```
test('Aggregate one search request across multiple enabled projects', () async { test(
'Aggregate one search request across multiple enabled projects while ignoring closed issues',
() async {
// Given two temporary project checkouts that can be used as local repo paths. // Given two temporary project checkouts that can be used as local repo paths.
final sandbox = await Directory.systemTemp.createTemp( final sandbox = await Directory.systemTemp.createTemp(
'cws-aggregate-search-', 'cws-aggregate-search-',
@ -1315,6 +1318,16 @@ projects:
'user': {'login': 'reporter-two'}, 'user': {'login': 'reporter-two'},
'labels': const [], 'labels': const [],
}, },
{
'number': 23,
'title': 'Repo two closed mention',
'body': 'closed repo issue',
'state': 'closed',
'html_url': 'https://example.test/owner/repo-two/issues/23',
'updated_at': '2026-04-04T17:02:00Z',
'user': {'login': 'reporter-three'},
'labels': const [],
},
], ],
}, },
commentResponsesByRepo: { commentResponsesByRepo: {
@ -1343,6 +1356,17 @@ projects:
'user': {'login': 'reporter-two'}, 'user': {'login': 'reporter-two'},
}, },
], ],
23: [
{
'id': 230,
'body': '@helper please review closed repo two',
'html_url':
'https://example.test/owner/repo-two/issues/23#issuecomment-230',
'created_at': '2026-04-04T17:02:00Z',
'updated_at': '2026-04-04T17:02:00Z',
'user': {'login': 'reporter-three'},
},
],
}, },
}, },
postCommentIssueNumbersByRepo: { postCommentIssueNumbersByRepo: {
@ -1362,7 +1386,9 @@ projects:
}); });
// And an orchestrator-style config with both repositories enabled. // And an orchestrator-style config with both repositories enabled.
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml')); final configFile = File(
p.join(sandbox.path, 'agent-orchestrator.yaml'),
);
await configFile.writeAsString(''' await configFile.writeAsString('''
defaults: defaults:
issueAssistant: issueAssistant:
@ -1390,12 +1416,13 @@ projects:
await app.runOnce(); await app.runOnce();
await app.close(); await app.close();
// Then it fetches issue updates through one aggregate GitHub search request. // Then it fetches issue updates through one aggregate GitHub search request for open issues only.
final logLines = await ghLog.readAsLines(); final logLines = await ghLog.readAsLines();
expect( final searchLogs = logLines
logLines.where((line) => line.contains('search/issues')).length, .where((line) => line.contains('search/issues'))
1, .toList();
); expect(searchLogs.length, 1);
expect(searchLogs.single, contains('state:open'));
// And it still posts one reply in each repository. // And it still posts one reply in each repository.
expect( expect(
@ -1416,6 +1443,16 @@ projects:
.length, .length,
2, 2,
); );
}); expect(
logLines
.where(
(line) =>
line.contains('repos/owner/repo-two/issues/23/comments'),
)
.length,
0,
);
},
);
}); });
} }

View File

@ -50,6 +50,19 @@ Future<void> writeFakeGhScript({
final rightUpdatedAt = right['updated_at'] as String? ?? ''; final rightUpdatedAt = right['updated_at'] as String? ?? '';
return leftUpdatedAt.compareTo(rightUpdatedAt); return leftUpdatedAt.compareTo(rightUpdatedAt);
}); });
final openSearchItems = searchItems
.where((item) => item['state'] == 'open')
.toList(growable: false);
final allSearchResponse = jsonEncode({
'total_count': searchItems.length,
'incomplete_results': false,
'items': searchItems,
});
final openSearchResponse = jsonEncode({
'total_count': openSearchItems.length,
'incomplete_results': false,
'items': openSearchItems,
});
final buffer = StringBuffer() final buffer = StringBuffer()
..writeln('#!/usr/bin/env bash') ..writeln('#!/usr/bin/env bash')
@ -79,15 +92,21 @@ Future<void> writeFakeGhScript({
buffer buffer
..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''') ..writeln(r'''if [[ "$1" == "api" && "$4" == "search/issues" ]]; then''')
..writeln(r''' query=""''')
..writeln(r''' for arg in "$@"; do''')
..writeln(r''' if [[ "$arg" == q=* ]]; then''')
..writeln(r''' query="${arg#q=}"''')
..writeln(r''' fi''')
..writeln(r''' done''')
..writeln(r''' if [[ "$query" == *"state:open"* ]]; then''')
..writeln(" cat <<'JSON'") ..writeln(" cat <<'JSON'")
..writeln( ..writeln(openSearchResponse)
jsonEncode({
'total_count': searchItems.length,
'incomplete_results': false,
'items': searchItems,
}),
)
..writeln('JSON') ..writeln('JSON')
..writeln(r''' else''')
..writeln(" cat <<'JSON'")
..writeln(allSearchResponse)
..writeln('JSON')
..writeln(r''' fi''')
..writeln(' exit 0') ..writeln(' exit 0')
..writeln('fi'); ..writeln('fi');