refactor: deduplicate gosmee event source and test scaffolding
This commit is contained in:
parent
22f2ad9c9d
commit
a1d0ffe8f3
|
|
@ -10,6 +10,7 @@ import '../client.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../../core/process_launcher.dart';
|
import '../../core/process_launcher.dart';
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
import 'gosmee_shared.dart';
|
||||||
|
|
||||||
class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
GiteaGosmeeIssueEventSource({
|
GiteaGosmeeIssueEventSource({
|
||||||
|
|
@ -20,7 +21,6 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
}) : _logger = logger;
|
}) : _logger = logger;
|
||||||
|
|
||||||
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
||||||
static const Duration _processShutdownTimeout = Duration(seconds: 2);
|
|
||||||
|
|
||||||
final IssueTrackerClient issueTrackerClient;
|
final IssueTrackerClient issueTrackerClient;
|
||||||
final String gosmeeCommand;
|
final String gosmeeCommand;
|
||||||
|
|
@ -207,7 +207,11 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
||||||
final processes = _gosmeeProcesses.values.toList(growable: false);
|
final processes = _gosmeeProcesses.values.toList(growable: false);
|
||||||
_gosmeeProcesses.clear();
|
_gosmeeProcesses.clear();
|
||||||
await _terminateProcesses(processes);
|
await GosmeeShared.terminateProcesses(
|
||||||
|
processes: processes,
|
||||||
|
sourceLabel: 'gitea/gosmee',
|
||||||
|
logError: _logError,
|
||||||
|
);
|
||||||
|
|
||||||
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
||||||
_webhookIdsByProject.clear();
|
_webhookIdsByProject.clear();
|
||||||
|
|
@ -311,57 +315,14 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _createGosmeeChannelUrl(String localUrl) async {
|
Future<String> _createGosmeeChannelUrl(String localUrl) async {
|
||||||
final result = await commandExecutor.run(gosmeeCommand, [
|
return GosmeeShared.createChannelUrl(
|
||||||
'--output',
|
commandExecutor: commandExecutor,
|
||||||
'json',
|
gosmeeCommand: gosmeeCommand,
|
||||||
'client',
|
localUrl: localUrl,
|
||||||
'--new-url',
|
sourceLabel: 'gitea/gosmee',
|
||||||
localUrl,
|
log: _log,
|
||||||
]);
|
logDebug: _logDebug,
|
||||||
_logDebug(
|
|
||||||
'gitea/gosmee channel_command_result local_url=$localUrl '
|
|
||||||
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
|
||||||
'stderr=${_summarizeText(result.stderr.toString())}',
|
|
||||||
);
|
);
|
||||||
if (result.exitCode != 0) {
|
|
||||||
throw ProcessException(
|
|
||||||
gosmeeCommand,
|
|
||||||
['--output', 'json', 'client', '--new-url', localUrl],
|
|
||||||
'${result.stdout}\n${result.stderr}',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final stdoutText = result.stdout.toString().trim();
|
|
||||||
if (stdoutText.isEmpty) {
|
|
||||||
throw const FormatException('gosmee did not print a public URL.');
|
|
||||||
}
|
|
||||||
_log(
|
|
||||||
'gitea/gosmee channel_raw_output local_url=$localUrl stdout=$stdoutText',
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
final decodedJson = jsonDecode(stdoutText);
|
|
||||||
if (decodedJson is Map<String, dynamic>) {
|
|
||||||
final publicUrl = decodedJson['url']?.toString().trim();
|
|
||||||
if (publicUrl == null || publicUrl.isEmpty) {
|
|
||||||
throw const FormatException(
|
|
||||||
'gosmee JSON output did not contain a valid public URL.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return publicUrl;
|
|
||||||
}
|
|
||||||
if (decodedJson is String) {
|
|
||||||
final publicUrl = decodedJson.trim();
|
|
||||||
if (publicUrl.isEmpty) {
|
|
||||||
throw const FormatException('gosmee did not print a public URL.');
|
|
||||||
}
|
|
||||||
return publicUrl;
|
|
||||||
}
|
|
||||||
} on FormatException {
|
|
||||||
if (Uri.tryParse(stdoutText)?.hasScheme ?? false) {
|
|
||||||
return stdoutText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw const FormatException('gosmee did not print a valid public URL.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleFlush() {
|
void _scheduleFlush() {
|
||||||
|
|
@ -425,14 +386,13 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
required Stream<List<int>> stream,
|
required Stream<List<int>> stream,
|
||||||
required String level,
|
required String level,
|
||||||
}) {
|
}) {
|
||||||
utf8.decoder.bind(stream).transform(const LineSplitter()).listen((line) {
|
GosmeeShared.pipeProcessLogs(
|
||||||
final message = 'gitea/gosmee project=${project.key} $level=$line';
|
logger: _logger,
|
||||||
if (level == 'stderr') {
|
sourceLabel: 'gitea/gosmee',
|
||||||
_logError(message);
|
projectKey: project.key,
|
||||||
return;
|
stream: stream,
|
||||||
}
|
level: level,
|
||||||
_log(message);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _log(String message) {
|
void _log(String message) {
|
||||||
|
|
@ -446,45 +406,6 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
||||||
void _logError(String message) {
|
void _logError(String message) {
|
||||||
_logger.error(message);
|
_logger.error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _terminateProcesses(
|
|
||||||
List<StartedExternalCommand> processes,
|
|
||||||
) async {
|
|
||||||
for (final process in processes) {
|
|
||||||
process.kill();
|
|
||||||
}
|
|
||||||
for (final process in processes) {
|
|
||||||
await _awaitProcessExit(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _awaitProcessExit(StartedExternalCommand process) async {
|
|
||||||
try {
|
|
||||||
await process.exitCode.timeout(_processShutdownTimeout);
|
|
||||||
return;
|
|
||||||
} on TimeoutException {
|
|
||||||
process.kill(ProcessSignal.sigkill);
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await process.exitCode.timeout(_processShutdownTimeout);
|
|
||||||
} on TimeoutException {
|
|
||||||
_logError(
|
|
||||||
'gitea/gosmee process did not exit after sigkill '
|
|
||||||
'timeout_ms=${_processShutdownTimeout.inMilliseconds}',
|
|
||||||
);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _summarizeText(String value) {
|
|
||||||
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
|
||||||
if (singleLine.length <= 240) {
|
|
||||||
return singleLine.isEmpty ? '(empty)' : singleLine;
|
|
||||||
}
|
|
||||||
return '${singleLine.substring(0, 240)}...';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueuedGiteaIssue {
|
class _QueuedGiteaIssue {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import '../client.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../../core/process_launcher.dart';
|
import '../../core/process_launcher.dart';
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
import 'gosmee_shared.dart';
|
||||||
|
|
||||||
class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
GitHubGosmeeIssueEventSource({
|
GitHubGosmeeIssueEventSource({
|
||||||
|
|
@ -20,7 +21,6 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
}) : _logger = logger;
|
}) : _logger = logger;
|
||||||
|
|
||||||
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
||||||
static const Duration _processShutdownTimeout = Duration(seconds: 2);
|
|
||||||
|
|
||||||
final IssueTrackerClient issueTrackerClient;
|
final IssueTrackerClient issueTrackerClient;
|
||||||
final String gosmeeCommand;
|
final String gosmeeCommand;
|
||||||
|
|
@ -207,7 +207,11 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
||||||
final processes = _gosmeeProcesses.values.toList(growable: false);
|
final processes = _gosmeeProcesses.values.toList(growable: false);
|
||||||
_gosmeeProcesses.clear();
|
_gosmeeProcesses.clear();
|
||||||
await _terminateProcesses(processes);
|
await GosmeeShared.terminateProcesses(
|
||||||
|
processes: processes,
|
||||||
|
sourceLabel: 'github/gosmee',
|
||||||
|
logError: _logError,
|
||||||
|
);
|
||||||
|
|
||||||
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
||||||
_webhookIdsByProject.clear();
|
_webhookIdsByProject.clear();
|
||||||
|
|
@ -311,60 +315,14 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _createGosmeeChannelUrl(String localUrl) async {
|
Future<String> _createGosmeeChannelUrl(String localUrl) async {
|
||||||
final result = await commandExecutor.run(gosmeeCommand, [
|
return GosmeeShared.createChannelUrl(
|
||||||
'--output',
|
commandExecutor: commandExecutor,
|
||||||
'json',
|
gosmeeCommand: gosmeeCommand,
|
||||||
'client',
|
localUrl: localUrl,
|
||||||
'--new-url',
|
sourceLabel: 'github/gosmee',
|
||||||
localUrl,
|
log: _log,
|
||||||
]);
|
logDebug: _logDebug,
|
||||||
_logDebug(
|
|
||||||
'github/gosmee channel_command_result local_url=$localUrl '
|
|
||||||
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
|
||||||
'stderr=${_summarizeText(result.stderr.toString())}',
|
|
||||||
);
|
);
|
||||||
if (result.exitCode != 0) {
|
|
||||||
throw ProcessException(
|
|
||||||
gosmeeCommand,
|
|
||||||
['--output', 'json', 'client', '--new-url', localUrl],
|
|
||||||
'${result.stdout}\n${result.stderr}',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final stdoutText = result.stdout.toString().trim();
|
|
||||||
if (stdoutText.isEmpty) {
|
|
||||||
throw const FormatException('gosmee did not print a public URL.');
|
|
||||||
}
|
|
||||||
_log(
|
|
||||||
'github/gosmee channel_raw_output local_url=$localUrl stdout=$stdoutText',
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(stdoutText);
|
|
||||||
if (decoded is Map<String, dynamic>) {
|
|
||||||
final url = decoded['url']?.toString().trim();
|
|
||||||
if (url == null || url.isEmpty) {
|
|
||||||
throw const FormatException(
|
|
||||||
'gosmee JSON output did not contain a valid public URL.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
if (decoded is String) {
|
|
||||||
final url = decoded.trim();
|
|
||||||
if (url.isEmpty) {
|
|
||||||
throw const FormatException('gosmee did not print a public URL.');
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
} on FormatException {
|
|
||||||
if (Uri.tryParse(stdoutText)?.hasScheme ?? false) {
|
|
||||||
return stdoutText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw const FormatException('gosmee did not print a valid public URL.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleFlush() {
|
void _scheduleFlush() {
|
||||||
|
|
@ -427,14 +385,13 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
required Stream<List<int>> stream,
|
required Stream<List<int>> stream,
|
||||||
required String level,
|
required String level,
|
||||||
}) {
|
}) {
|
||||||
utf8.decoder.bind(stream).transform(const LineSplitter()).listen((line) {
|
GosmeeShared.pipeProcessLogs(
|
||||||
final message = 'github/gosmee project=${project.key} $level=$line';
|
logger: _logger,
|
||||||
if (level == 'stderr') {
|
sourceLabel: 'github/gosmee',
|
||||||
_logger.error(message);
|
projectKey: project.key,
|
||||||
} else {
|
stream: stream,
|
||||||
_logger.info(message);
|
level: level,
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _log(String message) => _logger.info(message);
|
void _log(String message) => _logger.info(message);
|
||||||
|
|
@ -442,45 +399,6 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
||||||
void _logDebug(String message) => _logger.debug(message);
|
void _logDebug(String message) => _logger.debug(message);
|
||||||
|
|
||||||
void _logError(String message) => _logger.error(message);
|
void _logError(String message) => _logger.error(message);
|
||||||
|
|
||||||
Future<void> _terminateProcesses(
|
|
||||||
List<StartedExternalCommand> processes,
|
|
||||||
) async {
|
|
||||||
for (final process in processes) {
|
|
||||||
process.kill();
|
|
||||||
}
|
|
||||||
for (final process in processes) {
|
|
||||||
await _awaitProcessExit(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _awaitProcessExit(StartedExternalCommand process) async {
|
|
||||||
try {
|
|
||||||
await process.exitCode.timeout(_processShutdownTimeout);
|
|
||||||
return;
|
|
||||||
} on TimeoutException {
|
|
||||||
process.kill(ProcessSignal.sigkill);
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await process.exitCode.timeout(_processShutdownTimeout);
|
|
||||||
} on TimeoutException {
|
|
||||||
_logError(
|
|
||||||
'github/gosmee process did not exit after sigkill '
|
|
||||||
'timeout_ms=${_processShutdownTimeout.inMilliseconds}',
|
|
||||||
);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _summarizeText(String value) {
|
|
||||||
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
|
||||||
if (singleLine.length <= 240) {
|
|
||||||
return singleLine.isEmpty ? '(empty)' : singleLine;
|
|
||||||
}
|
|
||||||
return '${singleLine.substring(0, 240)}...';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueuedGitHubIssue {
|
class _QueuedGitHubIssue {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import '../../core/process_launcher.dart';
|
||||||
import '../client.dart';
|
import '../client.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
import 'gosmee_shared.dart';
|
||||||
|
|
||||||
class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
GitLabGosmeeIssueEventSource({
|
GitLabGosmeeIssueEventSource({
|
||||||
|
|
@ -18,7 +19,6 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
}) : _logger = logger;
|
}) : _logger = logger;
|
||||||
|
|
||||||
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
||||||
static const Duration _processShutdownTimeout = Duration(seconds: 2);
|
|
||||||
|
|
||||||
final IssueTrackerClient issueTrackerClient;
|
final IssueTrackerClient issueTrackerClient;
|
||||||
final String gosmeeCommand;
|
final String gosmeeCommand;
|
||||||
|
|
@ -205,7 +205,11 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
|
|
||||||
final processes = _gosmeeProcesses.values.toList(growable: false);
|
final processes = _gosmeeProcesses.values.toList(growable: false);
|
||||||
_gosmeeProcesses.clear();
|
_gosmeeProcesses.clear();
|
||||||
await _terminateProcesses(processes);
|
await GosmeeShared.terminateProcesses(
|
||||||
|
processes: processes,
|
||||||
|
sourceLabel: 'gitlab/gosmee',
|
||||||
|
logError: _logError,
|
||||||
|
);
|
||||||
|
|
||||||
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
||||||
_webhookIdsByProject.clear();
|
_webhookIdsByProject.clear();
|
||||||
|
|
@ -349,60 +353,14 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _createGosmeeChannelUrl(String localUrl) async {
|
Future<String> _createGosmeeChannelUrl(String localUrl) async {
|
||||||
final result = await commandExecutor.run(gosmeeCommand, [
|
return GosmeeShared.createChannelUrl(
|
||||||
'--output',
|
commandExecutor: commandExecutor,
|
||||||
'json',
|
gosmeeCommand: gosmeeCommand,
|
||||||
'client',
|
localUrl: localUrl,
|
||||||
'--new-url',
|
sourceLabel: 'gitlab/gosmee',
|
||||||
localUrl,
|
log: _log,
|
||||||
]);
|
logDebug: _logDebug,
|
||||||
_logDebug(
|
|
||||||
'gitlab/gosmee channel_command_result local_url=$localUrl '
|
|
||||||
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
|
||||||
'stderr=${_summarizeText(result.stderr.toString())}',
|
|
||||||
);
|
);
|
||||||
if (result.exitCode != 0) {
|
|
||||||
throw ProcessException(
|
|
||||||
gosmeeCommand,
|
|
||||||
['--output', 'json', 'client', '--new-url', localUrl],
|
|
||||||
'${result.stdout}\n${result.stderr}',
|
|
||||||
result.exitCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final stdoutText = result.stdout.toString().trim();
|
|
||||||
if (stdoutText.isEmpty) {
|
|
||||||
throw const FormatException('gosmee did not print a public URL.');
|
|
||||||
}
|
|
||||||
_log(
|
|
||||||
'gitlab/gosmee channel_raw_output local_url=$localUrl stdout=$stdoutText',
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final decoded = jsonDecode(stdoutText);
|
|
||||||
if (decoded is Map<String, dynamic>) {
|
|
||||||
final url = decoded['url']?.toString().trim();
|
|
||||||
if (url == null || url.isEmpty) {
|
|
||||||
throw const FormatException(
|
|
||||||
'gosmee JSON output did not contain a valid public URL.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
if (decoded is String) {
|
|
||||||
final url = decoded.trim();
|
|
||||||
if (url.isEmpty) {
|
|
||||||
throw const FormatException('gosmee did not print a public URL.');
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
} on FormatException {
|
|
||||||
if (Uri.tryParse(stdoutText)?.hasScheme ?? false) {
|
|
||||||
return stdoutText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw const FormatException('gosmee did not print a valid public URL.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleFlush() {
|
void _scheduleFlush() {
|
||||||
|
|
@ -465,14 +423,13 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
required Stream<List<int>> stream,
|
required Stream<List<int>> stream,
|
||||||
required String level,
|
required String level,
|
||||||
}) {
|
}) {
|
||||||
utf8.decoder.bind(stream).transform(const LineSplitter()).listen((line) {
|
GosmeeShared.pipeProcessLogs(
|
||||||
final message = 'gitlab/gosmee project=${project.key} $level=$line';
|
logger: _logger,
|
||||||
if (level == 'stderr') {
|
sourceLabel: 'gitlab/gosmee',
|
||||||
_logger.error(message);
|
projectKey: project.key,
|
||||||
} else {
|
stream: stream,
|
||||||
_logger.info(message);
|
level: level,
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _log(String message) => _logger.info(message);
|
void _log(String message) => _logger.info(message);
|
||||||
|
|
@ -480,45 +437,6 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
||||||
void _logDebug(String message) => _logger.debug(message);
|
void _logDebug(String message) => _logger.debug(message);
|
||||||
|
|
||||||
void _logError(String message) => _logger.error(message);
|
void _logError(String message) => _logger.error(message);
|
||||||
|
|
||||||
Future<void> _terminateProcesses(
|
|
||||||
List<StartedExternalCommand> processes,
|
|
||||||
) async {
|
|
||||||
for (final process in processes) {
|
|
||||||
process.kill();
|
|
||||||
}
|
|
||||||
for (final process in processes) {
|
|
||||||
await _awaitProcessExit(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _awaitProcessExit(StartedExternalCommand process) async {
|
|
||||||
try {
|
|
||||||
await process.exitCode.timeout(_processShutdownTimeout);
|
|
||||||
return;
|
|
||||||
} on TimeoutException {
|
|
||||||
process.kill(ProcessSignal.sigkill);
|
|
||||||
} catch (_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await process.exitCode.timeout(_processShutdownTimeout);
|
|
||||||
} on TimeoutException {
|
|
||||||
_logError(
|
|
||||||
'gitlab/gosmee process did not exit after sigkill '
|
|
||||||
'timeout_ms=${_processShutdownTimeout.inMilliseconds}',
|
|
||||||
);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _summarizeText(String value) {
|
|
||||||
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
|
||||||
if (singleLine.length <= 240) {
|
|
||||||
return singleLine.isEmpty ? '(empty)' : singleLine;
|
|
||||||
}
|
|
||||||
return '${singleLine.substring(0, 240)}...';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueuedGitLabIssue {
|
class _QueuedGitLabIssue {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../../config/app_logger.dart';
|
||||||
|
import '../../core/process_launcher.dart';
|
||||||
|
|
||||||
|
class GosmeeShared {
|
||||||
|
static const Duration processShutdownTimeout = Duration(seconds: 2);
|
||||||
|
|
||||||
|
static Future<String> createChannelUrl({
|
||||||
|
required ExternalCommandExecutor commandExecutor,
|
||||||
|
required String gosmeeCommand,
|
||||||
|
required String localUrl,
|
||||||
|
required String sourceLabel,
|
||||||
|
required void Function(String message) log,
|
||||||
|
required void Function(String message) logDebug,
|
||||||
|
}) async {
|
||||||
|
final result = await commandExecutor.run(gosmeeCommand, [
|
||||||
|
'--output',
|
||||||
|
'json',
|
||||||
|
'client',
|
||||||
|
'--new-url',
|
||||||
|
localUrl,
|
||||||
|
]);
|
||||||
|
logDebug(
|
||||||
|
'$sourceLabel channel_command_result local_url=$localUrl '
|
||||||
|
'exit_code=${result.exitCode} stdout=${_summarizeText(result.stdout.toString())} '
|
||||||
|
'stderr=${_summarizeText(result.stderr.toString())}',
|
||||||
|
);
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
gosmeeCommand,
|
||||||
|
['--output', 'json', 'client', '--new-url', localUrl],
|
||||||
|
'${result.stdout}\n${result.stderr}',
|
||||||
|
result.exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final stdoutText = result.stdout.toString().trim();
|
||||||
|
if (stdoutText.isEmpty) {
|
||||||
|
throw const FormatException('gosmee did not print a public URL.');
|
||||||
|
}
|
||||||
|
log(
|
||||||
|
'$sourceLabel channel_raw_output local_url=$localUrl stdout=$stdoutText',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(stdoutText);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
final url = decoded['url']?.toString().trim();
|
||||||
|
if (url == null || url.isEmpty) {
|
||||||
|
throw const FormatException(
|
||||||
|
'gosmee JSON output did not contain a valid public URL.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
if (decoded is String) {
|
||||||
|
final url = decoded.trim();
|
||||||
|
if (url.isEmpty) {
|
||||||
|
throw const FormatException('gosmee did not print a public URL.');
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
} on FormatException {
|
||||||
|
if (Uri.tryParse(stdoutText)?.hasScheme ?? false) {
|
||||||
|
return stdoutText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw const FormatException('gosmee did not print a valid public URL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> terminateProcesses({
|
||||||
|
required List<StartedExternalCommand> processes,
|
||||||
|
required String sourceLabel,
|
||||||
|
required void Function(String message) logError,
|
||||||
|
}) async {
|
||||||
|
for (final process in processes) {
|
||||||
|
process.kill();
|
||||||
|
}
|
||||||
|
for (final process in processes) {
|
||||||
|
await _awaitProcessExit(
|
||||||
|
process: process,
|
||||||
|
sourceLabel: sourceLabel,
|
||||||
|
logError: logError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pipeProcessLogs({
|
||||||
|
required AppLogger logger,
|
||||||
|
required String sourceLabel,
|
||||||
|
required String projectKey,
|
||||||
|
required Stream<List<int>> stream,
|
||||||
|
required String level,
|
||||||
|
}) {
|
||||||
|
utf8.decoder.bind(stream).transform(const LineSplitter()).listen((line) {
|
||||||
|
final message = '$sourceLabel project=$projectKey $level=$line';
|
||||||
|
if (level == 'stderr') {
|
||||||
|
logger.error(message);
|
||||||
|
} else {
|
||||||
|
logger.info(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _awaitProcessExit({
|
||||||
|
required StartedExternalCommand process,
|
||||||
|
required String sourceLabel,
|
||||||
|
required void Function(String message) logError,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await process.exitCode.timeout(processShutdownTimeout);
|
||||||
|
return;
|
||||||
|
} on TimeoutException {
|
||||||
|
process.kill(ProcessSignal.sigkill);
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await process.exitCode.timeout(processShutdownTimeout);
|
||||||
|
} on TimeoutException {
|
||||||
|
logError(
|
||||||
|
'$sourceLabel process did not exit after sigkill '
|
||||||
|
'timeout_ms=${processShutdownTimeout.inMilliseconds}',
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _summarizeText(String value) {
|
||||||
|
final singleLine = value.trim().replaceAll(RegExp(r'\s+'), ' ');
|
||||||
|
if (singleLine.length <= 240) {
|
||||||
|
return singleLine.isEmpty ? '(empty)' : singleLine;
|
||||||
|
}
|
||||||
|
return '${singleLine.substring(0, 240)}...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:path/path.dart' as p;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../test_support.dart';
|
import '../test_support.dart';
|
||||||
|
import 'test_helpers.dart';
|
||||||
|
|
||||||
void registerIssueAssistantAppRunGitHubGosmeeTests() {
|
void registerIssueAssistantAppRunGitHubGosmeeTests() {
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
|
|
@ -29,11 +30,9 @@ void registerIssueAssistantAppRunGitHubGosmeeTests() {
|
||||||
/// ```
|
/// ```
|
||||||
test('Reply to a mention received through gh/gosmee', () async {
|
test('Reply to a mention received through gh/gosmee', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(prefix: 'cws-gh-gosmee-run-');
|
||||||
'cws-gh-gosmee-run-',
|
final sandbox = sandboxRepo.sandbox;
|
||||||
);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
|
||||||
await initGitRepo(repoDir);
|
|
||||||
|
|
||||||
// And a gosmee script that forwards one GitHub issue_comment webhook event.
|
// And a gosmee script that forwards one GitHub issue_comment webhook event.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -95,26 +94,14 @@ void registerIssueAssistantAppRunGitHubGosmeeTests() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses gh/gosmee.
|
// And an orchestrator-style config that uses gh/gosmee.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'github',
|
||||||
enabled: true
|
repo: 'owner/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const ['pollInterval: 5m', 'type: gh/gosmee'],
|
||||||
- id: primary
|
);
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: github
|
|
||||||
eventSource:
|
|
||||||
pollInterval: 5m
|
|
||||||
type: gh/gosmee
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
config: await AppConfig.load(configFile.path),
|
config: await AppConfig.load(configFile.path),
|
||||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||||
|
|
@ -132,15 +119,7 @@ projects:
|
||||||
addTearDown(() => AppLogger.removeListener(captureLog));
|
addTearDown(() => AppLogger.removeListener(captureLog));
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
await runAppUntil(app: app, isReady: postedBody.exists);
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
|
||||||
if (await postedBody.exists()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then it reads issue comments and posts exactly one reply for that issue.
|
// Then it reads issue comments and posts exactly one reply for that issue.
|
||||||
expect(await postedBody.exists(), isTrue);
|
expect(await postedBody.exists(), isTrue);
|
||||||
|
|
@ -232,11 +211,11 @@ projects:
|
||||||
'Delete webhook even when gosmee ignores SIGTERM during shutdown',
|
'Delete webhook even when gosmee ignores SIGTERM during shutdown',
|
||||||
() async {
|
() async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-gh-gosmee-shutdown-',
|
prefix: 'cws-gh-gosmee-shutdown-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gosmee command that forwards one GitHub webhook event but ignores SIGTERM.
|
// And a gosmee command that forwards one GitHub webhook event but ignores SIGTERM.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -299,28 +278,14 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses gh/gosmee.
|
// And an orchestrator-style config that uses gh/gosmee.
|
||||||
final configFile = File(
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
p.join(sandbox.path, 'agent-orchestrator.yaml'),
|
sandbox: sandbox,
|
||||||
|
responderPath: responderScript.path,
|
||||||
|
provider: 'github',
|
||||||
|
repo: 'owner/sample',
|
||||||
|
repoPath: repoDir.path,
|
||||||
|
eventSourceLines: const ['pollInterval: 5m', 'type: gh/gosmee'],
|
||||||
);
|
);
|
||||||
await configFile.writeAsString('''
|
|
||||||
defaults:
|
|
||||||
issueAssistant:
|
|
||||||
enabled: true
|
|
||||||
mentionTriggers: ["@helper"]
|
|
||||||
responders:
|
|
||||||
- id: primary
|
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: github
|
|
||||||
eventSource:
|
|
||||||
pollInterval: 5m
|
|
||||||
type: gh/gosmee
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
config: await AppConfig.load(configFile.path),
|
config: await AppConfig.load(configFile.path),
|
||||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||||
|
|
@ -331,15 +296,7 @@ projects:
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the long-running app starts and then closes.
|
// When the long-running app starts and then closes.
|
||||||
final runFuture = app.run();
|
await runAppUntil(app: app, isReady: postedBody.exists);
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
|
||||||
if (await postedBody.exists()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then the app still deletes the GitHub webhook for the project.
|
// Then the app still deletes the GitHub webhook for the project.
|
||||||
final ghLogLines = await ghLog.readAsLines();
|
final ghLogLines = await ghLog.readAsLines();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:path/path.dart' as p;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../test_support.dart';
|
import '../test_support.dart';
|
||||||
|
import 'test_helpers.dart';
|
||||||
|
|
||||||
void registerIssueAssistantAppRunGitLabGosmeeTests() {
|
void registerIssueAssistantAppRunGitLabGosmeeTests() {
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
|
|
@ -29,11 +30,11 @@ void registerIssueAssistantAppRunGitLabGosmeeTests() {
|
||||||
/// ```
|
/// ```
|
||||||
test('Reply to a mention received through glab/gosmee', () async {
|
test('Reply to a mention received through glab/gosmee', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-glab-gosmee-run-',
|
prefix: 'cws-glab-gosmee-run-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gosmee script that forwards one GitLab note webhook event.
|
// And a gosmee script that forwards one GitLab note webhook event.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -109,26 +110,14 @@ void registerIssueAssistantAppRunGitLabGosmeeTests() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses glab/gosmee.
|
// And an orchestrator-style config that uses glab/gosmee.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'gitlab',
|
||||||
enabled: true
|
repo: 'group/subgroup/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const ['pollInterval: 5m', 'type: glab/gosmee'],
|
||||||
- id: primary
|
);
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: gitlab
|
|
||||||
eventSource:
|
|
||||||
pollInterval: 5m
|
|
||||||
type: glab/gosmee
|
|
||||||
repo: group/subgroup/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
config: await AppConfig.load(configFile.path),
|
config: await AppConfig.load(configFile.path),
|
||||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||||
|
|
@ -146,15 +135,7 @@ projects:
|
||||||
addTearDown(() => AppLogger.removeListener(captureLog));
|
addTearDown(() => AppLogger.removeListener(captureLog));
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
await runAppUntil(app: app, isReady: postedBody.exists);
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
|
||||||
if (await postedBody.exists()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then it reads issue comments and posts exactly one reply for that issue.
|
// Then it reads issue comments and posts exactly one reply for that issue.
|
||||||
expect(await postedBody.exists(), isTrue);
|
expect(await postedBody.exists(), isTrue);
|
||||||
|
|
@ -251,11 +232,11 @@ projects:
|
||||||
'Delete webhook even when gosmee ignores SIGTERM during shutdown',
|
'Delete webhook even when gosmee ignores SIGTERM during shutdown',
|
||||||
() async {
|
() async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-glab-gosmee-shutdown-',
|
prefix: 'cws-glab-gosmee-shutdown-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gosmee command that forwards one GitLab webhook event but ignores SIGTERM.
|
// And a gosmee command that forwards one GitLab webhook event but ignores SIGTERM.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -332,28 +313,14 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses glab/gosmee.
|
// And an orchestrator-style config that uses glab/gosmee.
|
||||||
final configFile = File(
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
p.join(sandbox.path, 'agent-orchestrator.yaml'),
|
sandbox: sandbox,
|
||||||
|
responderPath: responderScript.path,
|
||||||
|
provider: 'gitlab',
|
||||||
|
repo: 'group/subgroup/sample',
|
||||||
|
repoPath: repoDir.path,
|
||||||
|
eventSourceLines: const ['pollInterval: 5m', 'type: glab/gosmee'],
|
||||||
);
|
);
|
||||||
await configFile.writeAsString('''
|
|
||||||
defaults:
|
|
||||||
issueAssistant:
|
|
||||||
enabled: true
|
|
||||||
mentionTriggers: ["@helper"]
|
|
||||||
responders:
|
|
||||||
- id: primary
|
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: gitlab
|
|
||||||
eventSource:
|
|
||||||
pollInterval: 5m
|
|
||||||
type: glab/gosmee
|
|
||||||
repo: group/subgroup/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
config: await AppConfig.load(configFile.path),
|
config: await AppConfig.load(configFile.path),
|
||||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||||
|
|
@ -364,15 +331,7 @@ projects:
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the long-running app starts and then closes.
|
// When the long-running app starts and then closes.
|
||||||
final runFuture = app.run();
|
await runAppUntil(app: app, isReady: postedBody.exists);
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
|
||||||
if (await postedBody.exists()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then the app still deletes the GitLab webhook for the project.
|
// Then the app still deletes the GitLab webhook for the project.
|
||||||
final glabLogLines = await glabLog.readAsLines();
|
final glabLogLines = await glabLog.readAsLines();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:path/path.dart' as p;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../test_support.dart';
|
import '../test_support.dart';
|
||||||
|
import 'test_helpers.dart';
|
||||||
|
|
||||||
void registerIssueAssistantAppRunGosmeeTests() {
|
void registerIssueAssistantAppRunGosmeeTests() {
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
|
|
@ -30,9 +31,9 @@ void registerIssueAssistantAppRunGosmeeTests() {
|
||||||
/// ```
|
/// ```
|
||||||
test('Reply to a mention received through tea/gosmee', () async {
|
test('Reply to a mention received through tea/gosmee', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp('cws-gosmee-run-');
|
final sandboxRepo = await createSandboxRepo(prefix: 'cws-gosmee-run-');
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gosmee script that forwards one Gitea issue_comment webhook event.
|
// And a gosmee script that forwards one Gitea issue_comment webhook event.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -101,26 +102,14 @@ void registerIssueAssistantAppRunGosmeeTests() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses tea/gosmee.
|
// And an orchestrator-style config that uses tea/gosmee.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'gitea',
|
||||||
enabled: true
|
repo: 'owner/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const ['pollInterval: 5m', 'type: tea/gosmee'],
|
||||||
- id: primary
|
);
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: gitea
|
|
||||||
eventSource:
|
|
||||||
pollInterval: 5m
|
|
||||||
type: tea/gosmee
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
await File(p.join(sandbox.path, '.env')).writeAsString('''
|
await File(p.join(sandbox.path, '.env')).writeAsString('''
|
||||||
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
||||||
CWS_GITEA_TOKEN=test-token
|
CWS_GITEA_TOKEN=test-token
|
||||||
|
|
@ -141,15 +130,10 @@ CWS_GITEA_TOKEN=test-token
|
||||||
addTearDown(() => AppLogger.removeListener(captureLog));
|
addTearDown(() => AppLogger.removeListener(captureLog));
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
await runAppUntil(
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
app: app,
|
||||||
if (giteaServer.postedBodies.isNotEmpty) {
|
isReady: () async => giteaServer.postedBodies.isNotEmpty,
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then it reads issue comments and posts exactly one reply for that issue.
|
// Then it reads issue comments and posts exactly one reply for that issue.
|
||||||
expect(giteaServer.postedBodies, hasLength(1));
|
expect(giteaServer.postedBodies, hasLength(1));
|
||||||
|
|
@ -225,11 +209,11 @@ CWS_GITEA_TOKEN=test-token
|
||||||
/// ```
|
/// ```
|
||||||
test('Reconcile a missed Gitea gosmee event through polling', () async {
|
test('Reconcile a missed Gitea gosmee event through polling', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-gosmee-reconcile-',
|
prefix: 'cws-gosmee-reconcile-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gosmee script that starts forwarding without delivering an event.
|
// And a gosmee script that starts forwarding without delivering an event.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -296,27 +280,18 @@ CWS_GITEA_TOKEN=test-token
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses tea/gosmee with continuous reconciliation.
|
// And an orchestrator-style config that uses tea/gosmee with continuous reconciliation.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'gitea',
|
||||||
enabled: true
|
repo: 'owner/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const [
|
||||||
- id: primary
|
'type: tea/gosmee',
|
||||||
command: ${responderScript.path}
|
'reconciliation: continuous',
|
||||||
timeout: 1m
|
'reconcileInterval: 1s',
|
||||||
projects:
|
],
|
||||||
sample:
|
);
|
||||||
issueTracker:
|
|
||||||
provider: gitea
|
|
||||||
eventSource:
|
|
||||||
type: tea/gosmee
|
|
||||||
reconciliation: continuous
|
|
||||||
reconcileInterval: 1s
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
await File(p.join(sandbox.path, '.env')).writeAsString('''
|
await File(p.join(sandbox.path, '.env')).writeAsString('''
|
||||||
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
||||||
CWS_GITEA_TOKEN=test-token
|
CWS_GITEA_TOKEN=test-token
|
||||||
|
|
@ -330,15 +305,10 @@ CWS_GITEA_TOKEN=test-token
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the long-running app starts and enough time passes for reconciliation polling.
|
// When the long-running app starts and enough time passes for reconciliation polling.
|
||||||
final runFuture = app.run();
|
await runAppUntil(
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
app: app,
|
||||||
if (giteaServer.postedBodies.isNotEmpty) {
|
isReady: () async => giteaServer.postedBodies.isNotEmpty,
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then it posts exactly one reply for the issue discovered by polling.
|
// Then it posts exactly one reply for the issue discovered by polling.
|
||||||
expect(giteaServer.postedBodies, hasLength(1));
|
expect(giteaServer.postedBodies, hasLength(1));
|
||||||
|
|
@ -378,11 +348,11 @@ CWS_GITEA_TOKEN=test-token
|
||||||
/// ```
|
/// ```
|
||||||
test('Delete webhook even when gosmee ignores SIGTERM during shutdown', () async {
|
test('Delete webhook even when gosmee ignores SIGTERM during shutdown', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-gosmee-shutdown-',
|
prefix: 'cws-gosmee-shutdown-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gosmee command that forwards one Gitea webhook event but ignores SIGTERM.
|
// And a gosmee command that forwards one Gitea webhook event but ignores SIGTERM.
|
||||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||||
|
|
@ -451,26 +421,14 @@ CWS_GITEA_TOKEN=test-token
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses tea/gosmee.
|
// And an orchestrator-style config that uses tea/gosmee.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'gitea',
|
||||||
enabled: true
|
repo: 'owner/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const ['pollInterval: 5m', 'type: tea/gosmee'],
|
||||||
- id: primary
|
);
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: gitea
|
|
||||||
eventSource:
|
|
||||||
pollInterval: 5m
|
|
||||||
type: tea/gosmee
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
await File(p.join(sandbox.path, '.env')).writeAsString('''
|
await File(p.join(sandbox.path, '.env')).writeAsString('''
|
||||||
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
||||||
CWS_GITEA_TOKEN=test-token
|
CWS_GITEA_TOKEN=test-token
|
||||||
|
|
@ -484,15 +442,10 @@ CWS_GITEA_TOKEN=test-token
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the long-running app starts and then closes.
|
// When the long-running app starts and then closes.
|
||||||
final runFuture = app.run();
|
await runAppUntil(
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
app: app,
|
||||||
if (giteaServer.postedBodies.isNotEmpty) {
|
isReady: () async => giteaServer.postedBodies.isNotEmpty,
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then the app still deletes the Gitea webhook for the project.
|
// Then the app still deletes the Gitea webhook for the project.
|
||||||
expect(giteaServer.deletedWebhookIds, contains(81));
|
expect(giteaServer.deletedWebhookIds, contains(81));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:code_work_spawner/code_work_spawner.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../test_support.dart';
|
||||||
|
|
||||||
|
Future<({Directory sandbox, Directory repoDir})> createSandboxRepo({
|
||||||
|
required String prefix,
|
||||||
|
}) async {
|
||||||
|
final sandbox = await Directory.systemTemp.createTemp(prefix);
|
||||||
|
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
||||||
|
await initGitRepo(repoDir);
|
||||||
|
return (sandbox: sandbox, repoDir: repoDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> writeSingleProjectIssueAssistantConfig({
|
||||||
|
required Directory sandbox,
|
||||||
|
required String responderPath,
|
||||||
|
required String provider,
|
||||||
|
required String repo,
|
||||||
|
required String repoPath,
|
||||||
|
required List<String> eventSourceLines,
|
||||||
|
}) async {
|
||||||
|
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
||||||
|
final eventSourceYaml = eventSourceLines
|
||||||
|
.map((line) => ' $line')
|
||||||
|
.join('\n');
|
||||||
|
await configFile.writeAsString('''
|
||||||
|
defaults:
|
||||||
|
issueAssistant:
|
||||||
|
enabled: true
|
||||||
|
mentionTriggers: ["@helper"]
|
||||||
|
responders:
|
||||||
|
- id: primary
|
||||||
|
command: $responderPath
|
||||||
|
timeout: 1m
|
||||||
|
projects:
|
||||||
|
sample:
|
||||||
|
issueTracker:
|
||||||
|
provider: $provider
|
||||||
|
eventSource:
|
||||||
|
$eventSourceYaml
|
||||||
|
repo: $repo
|
||||||
|
path: $repoPath
|
||||||
|
''');
|
||||||
|
return configFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> runAppUntil({
|
||||||
|
required IssueAssistantApp app,
|
||||||
|
required Future<bool> Function() isReady,
|
||||||
|
int maxAttempts = 200,
|
||||||
|
Duration pollInterval = const Duration(milliseconds: 100),
|
||||||
|
}) async {
|
||||||
|
final runFuture = app.run();
|
||||||
|
try {
|
||||||
|
for (var attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||||
|
if (await isReady()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await Future<void>.delayed(pollInterval);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
await runFuture;
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:path/path.dart' as p;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../test_support.dart';
|
import '../test_support.dart';
|
||||||
|
import 'test_helpers.dart';
|
||||||
|
|
||||||
void registerIssueAssistantAppRunWebhookForwardTests() {
|
void registerIssueAssistantAppRunWebhookForwardTests() {
|
||||||
/// ```gherkin
|
/// ```gherkin
|
||||||
|
|
@ -29,9 +30,9 @@ void registerIssueAssistantAppRunWebhookForwardTests() {
|
||||||
/// ```
|
/// ```
|
||||||
test('Reply to a mention received through gh/webhook-forward', () async {
|
test('Reply to a mention received through gh/webhook-forward', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp('cws-webhook-run-');
|
final sandboxRepo = await createSandboxRepo(prefix: 'cws-webhook-run-');
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gh script that forwards one issue_comment webhook event.
|
// And a gh script that forwards one issue_comment webhook event.
|
||||||
final ghScript = File(p.join(sandbox.path, 'gh'));
|
final ghScript = File(p.join(sandbox.path, 'gh'));
|
||||||
|
|
@ -77,25 +78,14 @@ void registerIssueAssistantAppRunWebhookForwardTests() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses gh/webhook-forward.
|
// And an orchestrator-style config that uses gh/webhook-forward.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'github',
|
||||||
enabled: true
|
repo: 'owner/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const ['type: gh/webhook-forward'],
|
||||||
- id: primary
|
);
|
||||||
command: ${responderScript.path}
|
|
||||||
timeout: 1m
|
|
||||||
projects:
|
|
||||||
sample:
|
|
||||||
issueTracker:
|
|
||||||
provider: github
|
|
||||||
eventSource:
|
|
||||||
type: gh/webhook-forward
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
config: await AppConfig.load(configFile.path),
|
config: await AppConfig.load(configFile.path),
|
||||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||||
|
|
@ -105,15 +95,7 @@ projects:
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the long-running app starts and receives the forwarded webhook.
|
// When the long-running app starts and receives the forwarded webhook.
|
||||||
final runFuture = app.run();
|
await runAppUntil(app: app, isReady: postedBody.exists);
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
|
||||||
if (await postedBody.exists()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then it reads issue comments and posts exactly one reply for that issue.
|
// Then it reads issue comments and posts exactly one reply for that issue.
|
||||||
expect(await postedBody.exists(), isTrue);
|
expect(await postedBody.exists(), isTrue);
|
||||||
|
|
@ -146,11 +128,11 @@ projects:
|
||||||
/// ```
|
/// ```
|
||||||
test('Log webhook forward stderr at error level', () async {
|
test('Log webhook forward stderr at error level', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-webhook-stderr-',
|
prefix: 'cws-webhook-stderr-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gh script that writes a webhook creation failure to stderr while forwarding.
|
// And a gh script that writes a webhook creation failure to stderr while forwarding.
|
||||||
final ghScript = File(p.join(sandbox.path, 'gh'));
|
final ghScript = File(p.join(sandbox.path, 'gh'));
|
||||||
|
|
@ -215,17 +197,12 @@ projects:
|
||||||
AppLogger.addListener(captureLog);
|
AppLogger.addListener(captureLog);
|
||||||
|
|
||||||
// When the long-running app starts and webhook-forward emits that stderr line.
|
// When the long-running app starts and webhook-forward emits that stderr line.
|
||||||
final runFuture = app.run();
|
await runAppUntil(
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
app: app,
|
||||||
if (records.any(
|
isReady: () async => records.any(
|
||||||
(record) => record.message.contains('error creating webhook'),
|
(record) => record.message.contains('error creating webhook'),
|
||||||
)) {
|
),
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
AppLogger.removeListener(captureLog);
|
AppLogger.removeListener(captureLog);
|
||||||
|
|
||||||
// Then the app logs the webhook-forward stderr message at error level.
|
// Then the app logs the webhook-forward stderr message at error level.
|
||||||
|
|
@ -255,11 +232,11 @@ projects:
|
||||||
/// ```
|
/// ```
|
||||||
test('Reconcile a missed GitHub webhook event through polling', () async {
|
test('Reconcile a missed GitHub webhook event through polling', () async {
|
||||||
// Given a temporary project checkout that can be used as the local repo path.
|
// Given a temporary project checkout that can be used as the local repo path.
|
||||||
final sandbox = await Directory.systemTemp.createTemp(
|
final sandboxRepo = await createSandboxRepo(
|
||||||
'cws-webhook-reconcile-',
|
prefix: 'cws-webhook-reconcile-',
|
||||||
);
|
);
|
||||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
final sandbox = sandboxRepo.sandbox;
|
||||||
await initGitRepo(repoDir);
|
final repoDir = sandboxRepo.repoDir;
|
||||||
|
|
||||||
// And a gh script that starts webhook forwarding without delivering an event.
|
// And a gh script that starts webhook forwarding without delivering an event.
|
||||||
final ghScript = File(p.join(sandbox.path, 'gh'));
|
final ghScript = File(p.join(sandbox.path, 'gh'));
|
||||||
|
|
@ -380,27 +357,18 @@ projects:
|
||||||
});
|
});
|
||||||
|
|
||||||
// And an orchestrator-style config that uses gh/webhook-forward with continuous reconciliation.
|
// And an orchestrator-style config that uses gh/webhook-forward with continuous reconciliation.
|
||||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
final configFile = await writeSingleProjectIssueAssistantConfig(
|
||||||
await configFile.writeAsString('''
|
sandbox: sandbox,
|
||||||
defaults:
|
responderPath: responderScript.path,
|
||||||
issueAssistant:
|
provider: 'github',
|
||||||
enabled: true
|
repo: 'owner/sample',
|
||||||
mentionTriggers: ["@helper"]
|
repoPath: repoDir.path,
|
||||||
responders:
|
eventSourceLines: const [
|
||||||
- id: primary
|
'type: gh/webhook-forward',
|
||||||
command: ${responderScript.path}
|
'reconciliation: continuous',
|
||||||
timeout: 1m
|
'reconcileInterval: 1s',
|
||||||
projects:
|
],
|
||||||
sample:
|
);
|
||||||
issueTracker:
|
|
||||||
provider: github
|
|
||||||
eventSource:
|
|
||||||
type: gh/webhook-forward
|
|
||||||
reconciliation: continuous
|
|
||||||
reconcileInterval: 1s
|
|
||||||
repo: owner/sample
|
|
||||||
path: ${repoDir.path}
|
|
||||||
''');
|
|
||||||
final app = await IssueAssistantApp.open(
|
final app = await IssueAssistantApp.open(
|
||||||
config: await AppConfig.load(configFile.path),
|
config: await AppConfig.load(configFile.path),
|
||||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||||
|
|
@ -410,15 +378,7 @@ projects:
|
||||||
);
|
);
|
||||||
|
|
||||||
// When the long-running app starts and enough time passes for reconciliation polling.
|
// When the long-running app starts and enough time passes for reconciliation polling.
|
||||||
final runFuture = app.run();
|
await runAppUntil(app: app, isReady: postedBody.exists);
|
||||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
|
||||||
if (await postedBody.exists()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
await app.close();
|
|
||||||
await runFuture;
|
|
||||||
|
|
||||||
// Then it posts exactly one reply for the issue discovered by polling.
|
// Then it posts exactly one reply for the issue discovered by polling.
|
||||||
expect(await postedBody.exists(), isTrue);
|
expect(await postedBody.exists(), isTrue);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue