forked from bkinnightskytw/code_work_spawner
fix: unregister gosmee webhooks on forced shutdown (#47)
This commit is contained in:
parent
0e9bd11d5c
commit
22f2ad9c9d
|
|
@ -20,6 +20,7 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
|||
}) : _logger = logger;
|
||||
|
||||
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
||||
static const Duration _processShutdownTimeout = Duration(seconds: 2);
|
||||
|
||||
final IssueTrackerClient issueTrackerClient;
|
||||
final String gosmeeCommand;
|
||||
|
|
@ -206,12 +207,7 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
|||
|
||||
final processes = _gosmeeProcesses.values.toList(growable: false);
|
||||
_gosmeeProcesses.clear();
|
||||
for (final process in processes) {
|
||||
process.kill();
|
||||
}
|
||||
for (final process in processes) {
|
||||
await process.exitCode.catchError((_) => 0);
|
||||
}
|
||||
await _terminateProcesses(processes);
|
||||
|
||||
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
||||
_webhookIdsByProject.clear();
|
||||
|
|
@ -451,6 +447,37 @@ class GiteaGosmeeIssueEventSource implements IssueEventSource {
|
|||
_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) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
|||
}) : _logger = logger;
|
||||
|
||||
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
||||
static const Duration _processShutdownTimeout = Duration(seconds: 2);
|
||||
|
||||
final IssueTrackerClient issueTrackerClient;
|
||||
final String gosmeeCommand;
|
||||
|
|
@ -206,12 +207,7 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
|||
|
||||
final processes = _gosmeeProcesses.values.toList(growable: false);
|
||||
_gosmeeProcesses.clear();
|
||||
for (final process in processes) {
|
||||
process.kill();
|
||||
}
|
||||
for (final process in processes) {
|
||||
await process.exitCode.catchError((_) => 0);
|
||||
}
|
||||
await _terminateProcesses(processes);
|
||||
|
||||
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
||||
_webhookIdsByProject.clear();
|
||||
|
|
@ -447,6 +443,37 @@ class GitHubGosmeeIssueEventSource implements IssueEventSource {
|
|||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
|||
}) : _logger = logger;
|
||||
|
||||
static const Duration _webhookCoalescingWindow = Duration(milliseconds: 500);
|
||||
static const Duration _processShutdownTimeout = Duration(seconds: 2);
|
||||
|
||||
final IssueTrackerClient issueTrackerClient;
|
||||
final String gosmeeCommand;
|
||||
|
|
@ -204,12 +205,7 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
|||
|
||||
final processes = _gosmeeProcesses.values.toList(growable: false);
|
||||
_gosmeeProcesses.clear();
|
||||
for (final process in processes) {
|
||||
process.kill();
|
||||
}
|
||||
for (final process in processes) {
|
||||
await process.exitCode.catchError((_) => 0);
|
||||
}
|
||||
await _terminateProcesses(processes);
|
||||
|
||||
final webhookIds = Map<String, int>.from(_webhookIdsByProject);
|
||||
_webhookIdsByProject.clear();
|
||||
|
|
@ -485,6 +481,37 @@ class GitLabGosmeeIssueEventSource implements IssueEventSource {
|
|||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,141 @@ projects:
|
|||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
/// ```gherkin
|
||||
/// Scenario: Delete webhook even when gosmee ignores SIGTERM during shutdown
|
||||
/// Given a temporary project checkout that can be used as the local repo path
|
||||
/// And a gosmee command that forwards one GitHub webhook event but ignores SIGTERM
|
||||
/// And a gh script that can manage webhooks and post issue comments
|
||||
/// And a responder that emits a planning reply
|
||||
/// And an orchestrator-style config that uses gh/gosmee
|
||||
/// When the long-running app starts and then closes
|
||||
/// Then the app still deletes the GitHub webhook for the project
|
||||
/// ```
|
||||
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.
|
||||
final sandbox = await Directory.systemTemp.createTemp(
|
||||
'cws-gh-gosmee-shutdown-',
|
||||
);
|
||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
||||
await initGitRepo(repoDir);
|
||||
|
||||
// And a gosmee command that forwards one GitHub webhook event but ignores SIGTERM.
|
||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||
final issue = {
|
||||
'number': 21,
|
||||
'title': 'Shutdown should still unregister webhook',
|
||||
'body': 'Please react before shutdown.',
|
||||
'state': 'open',
|
||||
'html_url': 'https://example.test/issues/21',
|
||||
'updated_at': '2026-04-08T07:00:00Z',
|
||||
'user': {'login': 'reporter'},
|
||||
'labels': const <Map<String, Object?>>[],
|
||||
};
|
||||
final comments = [
|
||||
{
|
||||
'id': 151,
|
||||
'body': '@helper verify webhook cleanup on shutdown',
|
||||
'html_url': 'https://example.test/issues/21#issuecomment-151',
|
||||
'created_at': '2026-04-08T07:00:00Z',
|
||||
'updated_at': '2026-04-08T07:00:00Z',
|
||||
'user': {'login': 'reporter'},
|
||||
},
|
||||
];
|
||||
registerFakeGosmeeCommand(
|
||||
command: gosmeeScript.path,
|
||||
publicUrl: 'https://gosmee.example.test/github-shutdown',
|
||||
payload: {
|
||||
'action': 'created',
|
||||
'issue': issue,
|
||||
'comment': comments.last,
|
||||
'repository': {'full_name': 'owner/sample'},
|
||||
},
|
||||
webhookHeader: 'X-GitHub-Event',
|
||||
exitOnSigterm: false,
|
||||
);
|
||||
|
||||
// And a gh script that can manage webhooks and post issue comments.
|
||||
final ghScript = File(p.join(sandbox.path, 'gh'));
|
||||
final ghLog = File(p.join(sandbox.path, 'gh-log.jsonl'));
|
||||
final postedBody = File(p.join(sandbox.path, 'posted-body.md'));
|
||||
await writeGitHubGosmeeGhScript(
|
||||
ghScript: ghScript,
|
||||
repo: 'owner/sample',
|
||||
webhookId: 81,
|
||||
issueNumber: 21,
|
||||
comments: comments,
|
||||
ghLog: ghLog,
|
||||
postedBodyFile: postedBody,
|
||||
viewerLogin: 'octocat',
|
||||
);
|
||||
|
||||
// And a responder that emits a planning reply.
|
||||
final responderScript = File(p.join(sandbox.path, 'responder.sh'));
|
||||
await writeResponderScript(responderScript, {
|
||||
'decision': 'reply',
|
||||
'mode': 'planning',
|
||||
'markdown':
|
||||
'Plan:\n- ensure webhook cleanup when shutdown races forwarding',
|
||||
'summary': 'posted shutdown cleanup advice',
|
||||
});
|
||||
|
||||
// And an orchestrator-style config that uses gh/gosmee.
|
||||
final configFile = File(
|
||||
p.join(sandbox.path, 'agent-orchestrator.yaml'),
|
||||
);
|
||||
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(
|
||||
config: await AppConfig.load(configFile.path),
|
||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||
ghCommand: ghScript.path,
|
||||
gosmeeCommand: gosmeeScript.path,
|
||||
responderRunner: fakeResponderRunner(),
|
||||
commandExecutor: fakeExternalCommandExecutor(),
|
||||
);
|
||||
|
||||
// When the long-running app starts and then closes.
|
||||
final runFuture = app.run();
|
||||
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.
|
||||
final ghLogLines = await ghLog.readAsLines();
|
||||
expect(
|
||||
ghLogLines.any(
|
||||
(line) =>
|
||||
line.contains('api -X DELETE repos/owner/sample/hooks/81'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -236,6 +236,156 @@ projects:
|
|||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
/// ```gherkin
|
||||
/// Scenario: Delete webhook even when gosmee ignores SIGTERM during shutdown
|
||||
/// Given a temporary project checkout that can be used as the local repo path
|
||||
/// And a gosmee command that forwards one GitLab webhook event but ignores SIGTERM
|
||||
/// And a glab script that can manage webhooks and post issue comments
|
||||
/// And a responder that emits a planning reply
|
||||
/// And an orchestrator-style config that uses glab/gosmee
|
||||
/// When the long-running app starts and then closes
|
||||
/// Then the app still deletes the GitLab webhook for the project
|
||||
/// ```
|
||||
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.
|
||||
final sandbox = await Directory.systemTemp.createTemp(
|
||||
'cws-glab-gosmee-shutdown-',
|
||||
);
|
||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
||||
await initGitRepo(repoDir);
|
||||
|
||||
// And a gosmee command that forwards one GitLab webhook event but ignores SIGTERM.
|
||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||
final issue = <String, Object?>{
|
||||
'iid': 21,
|
||||
'title': 'Shutdown should still unregister webhook',
|
||||
'description': 'Please react before shutdown.',
|
||||
'state': 'opened',
|
||||
'web_url':
|
||||
'https://gitlab.example.test/group/subgroup/sample/-/issues/21',
|
||||
'updated_at': '2026-04-08T07:00:00Z',
|
||||
'author': {'username': 'reporter'},
|
||||
'labels': const <String>[],
|
||||
};
|
||||
final comments = <Map<String, Object?>>[
|
||||
{
|
||||
'id': 151,
|
||||
'body': '@helper verify webhook cleanup on shutdown',
|
||||
'created_at': '2026-04-08T07:00:00Z',
|
||||
'updated_at': '2026-04-08T07:00:00Z',
|
||||
'author': {'username': 'reporter'},
|
||||
'noteable_url':
|
||||
'https://gitlab.example.test/group/subgroup/sample/-/issues/21',
|
||||
},
|
||||
];
|
||||
registerFakeGosmeeCommand(
|
||||
command: gosmeeScript.path,
|
||||
publicUrl: 'https://gosmee.example.test/gitlab-shutdown',
|
||||
payload: {
|
||||
'object_kind': 'note',
|
||||
'project': {'path_with_namespace': 'group/subgroup/sample'},
|
||||
'issue': issue,
|
||||
'object_attributes': {
|
||||
'action': 'create',
|
||||
'note': comments.last['body'],
|
||||
'noteable_type': 'Issue',
|
||||
'url': comments.last['noteable_url'],
|
||||
},
|
||||
'user': {'username': 'reporter'},
|
||||
},
|
||||
webhookHeader: 'X-Gitlab-Event',
|
||||
exitOnSigterm: false,
|
||||
);
|
||||
|
||||
// And a glab script that can manage webhooks and post issue comments.
|
||||
final glabScript = File(p.join(sandbox.path, 'glab'));
|
||||
final glabLog = File(p.join(sandbox.path, 'glab-log.jsonl'));
|
||||
final postedBody = File(p.join(sandbox.path, 'posted-body.md'));
|
||||
await writeFakeGlabScript(
|
||||
glabScript: glabScript,
|
||||
issueListResponsesByRepo: {
|
||||
'group/subgroup/sample': const <Map<String, Object?>>[],
|
||||
},
|
||||
commentResponsesByRepo: {
|
||||
'group/subgroup/sample': {21: comments},
|
||||
},
|
||||
postCommentIssueNumbersByRepo: {
|
||||
'group/subgroup/sample': {21},
|
||||
},
|
||||
glabLog: glabLog,
|
||||
postedBodyFile: postedBody,
|
||||
viewerLogin: 'glab-user',
|
||||
webhookIdsByRepo: {'group/subgroup/sample': 81},
|
||||
);
|
||||
|
||||
// And a responder that emits a planning reply.
|
||||
final responderScript = File(p.join(sandbox.path, 'responder.sh'));
|
||||
await writeResponderScript(responderScript, {
|
||||
'decision': 'reply',
|
||||
'mode': 'planning',
|
||||
'markdown':
|
||||
'Plan:\n- ensure webhook cleanup when shutdown races forwarding',
|
||||
'summary': 'posted shutdown cleanup advice',
|
||||
});
|
||||
|
||||
// And an orchestrator-style config that uses glab/gosmee.
|
||||
final configFile = File(
|
||||
p.join(sandbox.path, 'agent-orchestrator.yaml'),
|
||||
);
|
||||
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(
|
||||
config: await AppConfig.load(configFile.path),
|
||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||
glabCommand: glabScript.path,
|
||||
gosmeeCommand: gosmeeScript.path,
|
||||
responderRunner: fakeResponderRunner(),
|
||||
commandExecutor: fakeExternalCommandExecutor(),
|
||||
);
|
||||
|
||||
// When the long-running app starts and then closes.
|
||||
final runFuture = app.run();
|
||||
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.
|
||||
final glabLogLines = await glabLog.readAsLines();
|
||||
expect(
|
||||
glabLogLines.any(
|
||||
(line) => line.contains(
|
||||
'api -X DELETE projects/group%2Fsubgroup%2Fsample/hooks/81',
|
||||
),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -365,6 +365,138 @@ CWS_GITEA_TOKEN=test-token
|
|||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
/// ```gherkin
|
||||
/// Scenario: Delete webhook even when gosmee ignores SIGTERM during shutdown
|
||||
/// Given a temporary project checkout that can be used as the local repo path
|
||||
/// And a gosmee command that forwards one Gitea webhook event but ignores SIGTERM
|
||||
/// And a Gitea API fixture that can manage webhooks and post issue comments
|
||||
/// And a responder that emits a planning reply
|
||||
/// And an orchestrator-style config that uses tea/gosmee
|
||||
/// When the long-running app starts and then closes
|
||||
/// Then the app still deletes the Gitea webhook for the project
|
||||
/// ```
|
||||
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.
|
||||
final sandbox = await Directory.systemTemp.createTemp(
|
||||
'cws-gosmee-shutdown-',
|
||||
);
|
||||
final repoDir = await Directory(p.join(sandbox.path, 'repo')).create();
|
||||
await initGitRepo(repoDir);
|
||||
|
||||
// And a gosmee command that forwards one Gitea webhook event but ignores SIGTERM.
|
||||
final gosmeeScript = File(p.join(sandbox.path, 'gosmee'));
|
||||
final issue = {
|
||||
'index': 21,
|
||||
'title': 'Shutdown should still unregister webhook',
|
||||
'body': 'Please react before shutdown.',
|
||||
'state': 'open',
|
||||
'url': 'https://gitea.example.test/owner/sample/issues/21',
|
||||
'updated_at': '2026-04-08T07:00:00Z',
|
||||
'poster': {'login': 'reporter'},
|
||||
'labels': const <Map<String, Object?>>[],
|
||||
};
|
||||
final comments = [
|
||||
{
|
||||
'id': 151,
|
||||
'body': '@helper verify webhook cleanup on shutdown',
|
||||
'url':
|
||||
'https://gitea.example.test/owner/sample/issues/21#issuecomment-151',
|
||||
'created_at': '2026-04-08T07:00:00Z',
|
||||
'updated_at': '2026-04-08T07:00:00Z',
|
||||
'poster': {'login': 'reporter'},
|
||||
},
|
||||
];
|
||||
registerFakeGosmeeCommand(
|
||||
command: gosmeeScript.path,
|
||||
publicUrl: 'https://gosmee.example.test/shutdown',
|
||||
payload: {
|
||||
'action': 'created',
|
||||
'issue': issue,
|
||||
'comment': comments.last,
|
||||
'repository': {'full_name': 'owner/sample'},
|
||||
},
|
||||
exitOnSigterm: false,
|
||||
);
|
||||
|
||||
// And a Gitea API fixture that can manage webhooks and post issue comments.
|
||||
final giteaServer = await FakeGiteaApiServer.start(
|
||||
issueListResponsesByRepo: {
|
||||
'owner/sample': const <Map<String, Object?>>[],
|
||||
},
|
||||
commentResponsesByRepo: {
|
||||
'owner/sample': {21: comments},
|
||||
},
|
||||
postCommentResponsesByRepo: {
|
||||
'owner/sample': {
|
||||
21: {
|
||||
'id': 803,
|
||||
'body': 'posted',
|
||||
'html_url': 'https://gitea.example.test/comment/803',
|
||||
},
|
||||
},
|
||||
},
|
||||
viewerLogin: 'tea-octocat',
|
||||
);
|
||||
addTearDown(giteaServer.close);
|
||||
|
||||
// And a responder that emits a planning reply.
|
||||
final responderScript = File(p.join(sandbox.path, 'responder.sh'));
|
||||
await writeResponderScript(responderScript, {
|
||||
'decision': 'reply',
|
||||
'mode': 'planning',
|
||||
'markdown':
|
||||
'Plan:\n- ensure webhook cleanup when shutdown races forwarding',
|
||||
'summary': 'posted shutdown cleanup advice',
|
||||
});
|
||||
|
||||
// And an orchestrator-style config that uses tea/gosmee.
|
||||
final configFile = File(p.join(sandbox.path, 'agent-orchestrator.yaml'));
|
||||
await configFile.writeAsString('''
|
||||
defaults:
|
||||
issueAssistant:
|
||||
enabled: true
|
||||
mentionTriggers: ["@helper"]
|
||||
responders:
|
||||
- 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('''
|
||||
CWS_GITEA_HOST=${giteaServer.baseUrl}
|
||||
CWS_GITEA_TOKEN=test-token
|
||||
''');
|
||||
final app = await IssueAssistantApp.open(
|
||||
config: await AppConfig.load(configFile.path),
|
||||
databasePath: p.join(sandbox.path, 'state.sqlite3'),
|
||||
gosmeeCommand: gosmeeScript.path,
|
||||
responderRunner: fakeResponderRunner(),
|
||||
commandExecutor: fakeExternalCommandExecutor(),
|
||||
);
|
||||
|
||||
// When the long-running app starts and then closes.
|
||||
final runFuture = app.run();
|
||||
for (var attempt = 0; attempt < 200; attempt += 1) {
|
||||
if (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.
|
||||
expect(giteaServer.deletedWebhookIds, contains(81));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ void registerFakeGosmeeCommand({
|
|||
Map<String, Object?>? payload,
|
||||
File? gosmeeLog,
|
||||
String webhookHeader = 'X-Gitea-Event',
|
||||
bool exitOnSigterm = true,
|
||||
}) {
|
||||
fakeCommandHandlers[command] = (arguments, {timeout}) async {
|
||||
if (gosmeeLog != null) {
|
||||
|
|
@ -44,7 +45,7 @@ void registerFakeGosmeeCommand({
|
|||
if (gosmeeLog != null) {
|
||||
await _appendLine(gosmeeLog, '${arguments.join(' ')}\n');
|
||||
}
|
||||
final process = FakeStartedExternalCommand();
|
||||
final process = FakeStartedExternalCommand(exitOnSigterm: exitOnSigterm);
|
||||
if (arguments.length >= 3 &&
|
||||
arguments[0] == 'client' &&
|
||||
arguments[1] == publicUrl) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ ExternalCommandExecutor fakeExternalCommandExecutor() {
|
|||
}
|
||||
|
||||
class FakeStartedExternalCommand implements StartedExternalCommand {
|
||||
FakeStartedExternalCommand({
|
||||
this.exitOnSigterm = true,
|
||||
this.exitOnSigkill = true,
|
||||
this.sigkillExitCode = 137,
|
||||
});
|
||||
|
||||
final bool exitOnSigterm;
|
||||
final bool exitOnSigkill;
|
||||
final int sigkillExitCode;
|
||||
final StreamController<List<int>> _stdoutController = StreamController();
|
||||
final StreamController<List<int>> _stderrController = StreamController();
|
||||
final Completer<int> _exitCodeCompleter = Completer<int>();
|
||||
|
|
@ -96,7 +105,16 @@ class FakeStartedExternalCommand implements StartedExternalCommand {
|
|||
|
||||
@override
|
||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
|
||||
complete();
|
||||
if (signal == ProcessSignal.sigkill) {
|
||||
if (exitOnSigkill) {
|
||||
complete(sigkillExitCode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (signal == ProcessSignal.sigterm && exitOnSigterm) {
|
||||
complete();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue