fix: infer tracker provider from event source (#38)

This commit is contained in:
insleker 2026-04-10 09:14:11 +08:00
parent f1eea7abd8
commit da256d068c
12 changed files with 456 additions and 319 deletions

View File

@ -6,11 +6,13 @@ Project entries can override `issueAssistant` fields when a repo needs a
different prompt, mention triggers, responder chain, notification list, or
event source.
Each project also has a separate SCM config. `issueTracker.provider` selects
the issue tracker integration, while `scm.provider` selects how temporary repo
workspaces are prepared for responders. Legacy top-level project `provider` is
still accepted during load for backward compatibility. When `scm` is omitted
it defaults to a matching provider for existing GitHub, GitLab, and Gitea configs.
Each project also has separate tracker and SCM config. `issueTracker.provider`
selects the logical tracker family, `issueTracker.eventSource.type` selects the
concrete adapter used to receive updates, and `scm.provider` selects how
temporary repo workspaces are prepared for responders. Legacy top-level project
`provider` and legacy `issueAssistant.eventSource` are still accepted during
load for backward compatibility. When `scm` is omitted it defaults to a
matching provider for existing GitHub, GitLab, and Gitea configs.
GitHub projects default to `gh/polling`, GitLab projects default to
`glab/polling`, and Gitea projects default to `tea/polling`. OpenProject
@ -32,11 +34,12 @@ recovery, or use `reconciliation: startup-only` to skip background polling
after startup. `--once` still uses a single polling reconciliation cycle so
existing backlog handling keeps working.
Use a nested `issueAssistant.eventSource` object so source-specific fields stay
grouped with the selected source type. For example:
Use a nested `issueTracker.eventSource` object so adapter-specific fields stay
grouped with the selected adapter key. For example:
```yaml
issueAssistant:
issueTracker:
provider: github
eventSource:
type: gh/webhook-forward
reconciliation: continuous
@ -44,7 +47,8 @@ issueAssistant:
```
```yaml
issueAssistant:
issueTracker:
provider: github
eventSource:
type: gh/polling
pollInterval: 30s

View File

@ -7,14 +7,6 @@ worktreeDir: ~/.worktrees
defaults:
issueAssistant:
enabled: true
# Use tea/gosmee for long-running runs.
eventSource:
type: tea/polling
pollInterval: 30s
# eventSource:
# type: tea/gosmee
# reconciliation: startup-only
# reconcileInterval: 30s
maxConcurrentIssues: 3
mentionTriggers: ["@codex", "@helper"]
responders:
@ -27,6 +19,14 @@ projects:
sample-gitea:
issueTracker:
provider: gitea
# Use tea/gosmee for long-running runs.
eventSource:
type: tea/polling
pollInterval: 30s
# eventSource:
# type: tea/gosmee
# reconciliation: startup-only
# reconcileInterval: 30s
scm:
provider: gitea
repo: owner/repo

View File

@ -7,14 +7,6 @@ worktreeDir: ~/.worktrees
defaults:
issueAssistant:
enabled: true
# Use gh/gosmee or gh/webhook-forward for long-running runs.
eventSource:
type: gh/polling
pollInterval: 30s
# eventSource:
# type: gh/gosmee
# reconciliation: startup-only
# reconcileInterval: 30s
maxConcurrentIssues: 3
mentionTriggers: ["@codex", "@helper"]
commentPrefixTemplate: |
@ -53,6 +45,14 @@ projects:
code-work-spawner:
issueTracker:
provider: github
# Use gh/gosmee or gh/webhook-forward for long-running runs.
eventSource:
type: gh/polling
pollInterval: 30s
# eventSource:
# type: gh/gosmee
# reconciliation: startup-only
# reconcileInterval: 30s
scm:
provider: github
repo: existedinnettw/code_work_spawner

View File

@ -7,10 +7,6 @@ worktreeDir: ~/.worktrees
defaults:
issueAssistant:
enabled: true
# GitLab currently supports polling.
eventSource:
type: glab/polling
pollInterval: 30s
maxConcurrentIssues: 3
mentionTriggers: ["@codex", "@helper"]
responders:
@ -23,6 +19,10 @@ projects:
sample-gitlab:
issueTracker:
provider: gitlab
# GitLab currently supports polling.
eventSource:
type: glab/polling
pollInterval: 30s
scm:
provider: gitlab
repo: group/subgroup/repo

View File

@ -8,9 +8,6 @@ worktreeDir: ~/.worktrees
defaults:
issueAssistant:
enabled: true
eventSource:
type: op/polling
pollInterval: 30s
maxConcurrentIssues: 3
mentionTriggers: ["@codex", "@helper"]
responders:
@ -23,6 +20,9 @@ projects:
sample-openproject:
issueTracker:
provider: openproject
eventSource:
type: op/polling
pollInterval: 30s
scm:
provider: github
# OpenProject project name or numeric project id.

View File

@ -11,8 +11,8 @@ import 'config_schema.dart';
export 'config_schema.dart'
show
AssistantCapability,
IssueAssistantEventSourceKind,
IssueAssistantReconciliationKind,
IssueTrackerEventSourceKind,
IssueTrackerReconciliationKind,
IssueTrackerProvider,
ScmProvider,
NotificationConfigDocument,
@ -139,7 +139,7 @@ class AppConfig {
issueAssistant: IssueAssistantConfigDocument(
enabled: true,
eventSource: IssueAssistantEventSourceDocument(
type: IssueAssistantEventSourceKind.ghPolling,
type: IssueTrackerEventSourceKind.ghPolling,
pollInterval: '30s',
),
maxConcurrentIssues: 3,
@ -202,10 +202,33 @@ class ProjectConfig {
);
}
final provider =
document.issueTracker?.provider ??
document.provider ??
IssueTrackerProvider.github;
final resolvedIssueAssistantDocument =
_resolveProjectIssueAssistantDocument(
key: key,
issueTracker: document.issueTracker,
issueAssistant: document.issueAssistant,
);
final hasExplicitEventSourceType =
resolvedIssueAssistantDocument?.eventSource?.type != null;
var issueAssistant = defaultAssistant.merge(resolvedIssueAssistantDocument);
final configuredProvider =
document.issueTracker?.provider ?? document.provider;
final inferredProvider = _issueTrackerProviderForEventSource(
issueAssistant.eventSource.type,
);
if (hasExplicitEventSourceType &&
configuredProvider != null &&
configuredProvider != inferredProvider) {
throw FormatException(
'projects.$key.issueAssistant.eventSource '
'${_eventSourceConfigValue(issueAssistant.eventSource.type)} '
'implies issue tracker provider ${inferredProvider.name}, '
'but the legacy configured provider is ${configuredProvider.name}.',
);
}
final provider = hasExplicitEventSourceType
? inferredProvider
: configuredProvider ?? inferredProvider;
if (provider == IssueTrackerProvider.gitlab) {
_validateGitLabProjectPath(repo, field: 'projects.$key.repo');
}
@ -213,15 +236,14 @@ class ProjectConfig {
if (document.scm?.provider == null && fallbackScmProvider == null) {
throw FormatException(
'projects.$key.scm.provider is required for '
'issueTracker.provider: ${provider.name}.',
'issue tracking provider: ${provider.name}.',
);
}
final scm = ScmConfig.fromDocument(
document.scm,
fallbackProvider: fallbackScmProvider,
);
var issueAssistant = defaultAssistant.merge(document.issueAssistant);
if (document.issueAssistant?.eventSource?.type == null &&
if (!hasExplicitEventSourceType &&
issueAssistant.eventSource is PollingIssueEventSourceConfig) {
issueAssistant = issueAssistant.copyWith(
eventSource:
@ -229,58 +251,17 @@ class ProjectConfig {
.copyWith(
type: switch (provider) {
IssueTrackerProvider.github =>
IssueAssistantEventSourceKind.ghPolling,
IssueTrackerEventSourceKind.ghPolling,
IssueTrackerProvider.gitlab =>
IssueAssistantEventSourceKind.glabPolling,
IssueTrackerEventSourceKind.glabPolling,
IssueTrackerProvider.gitea =>
IssueAssistantEventSourceKind.teaPolling,
IssueTrackerEventSourceKind.teaPolling,
IssueTrackerProvider.openproject =>
IssueAssistantEventSourceKind.opPolling,
IssueTrackerEventSourceKind.opPolling,
},
),
);
}
switch (issueAssistant.eventSource.type) {
case IssueAssistantEventSourceKind.ghPolling:
case IssueAssistantEventSourceKind.ghGosmee:
case IssueAssistantEventSourceKind.ghWebhookForward:
if (provider != IssueTrackerProvider.github) {
throw FormatException(
'projects.$key.issueAssistant.eventSource '
'${_eventSourceConfigValue(issueAssistant.eventSource.type)} '
'requires issueTracker.provider: github.',
);
}
case IssueAssistantEventSourceKind.glabPolling:
if (provider != IssueTrackerProvider.gitlab) {
throw FormatException(
'projects.$key.issueAssistant.eventSource glab/polling '
'requires issueTracker.provider: gitlab.',
);
}
case IssueAssistantEventSourceKind.teaPolling:
if (provider != IssueTrackerProvider.gitea) {
throw FormatException(
'projects.$key.issueAssistant.eventSource tea/polling '
'requires issueTracker.provider: gitea.',
);
}
case IssueAssistantEventSourceKind.teaGosmee:
if (provider != IssueTrackerProvider.gitea) {
throw FormatException(
'projects.$key.issueAssistant.eventSource tea/gosmee '
'requires issueTracker.provider: gitea.',
);
}
case IssueAssistantEventSourceKind.opPolling:
if (provider != IssueTrackerProvider.openproject) {
throw FormatException(
'projects.$key.issueAssistant.eventSource op/polling '
'requires issueTracker.provider: openproject.',
);
}
}
if (provider == IssueTrackerProvider.openproject && repo.trim().isEmpty) {
throw FormatException(
'Invalid OpenProject project reference for projects.$key.repo: '
@ -343,6 +324,49 @@ class ProjectConfig {
}
}
IssueAssistantConfigDocument? _resolveProjectIssueAssistantDocument({
required String key,
required IssueTrackerConfigDocument? issueTracker,
required IssueAssistantConfigDocument? issueAssistant,
}) {
final trackerEventSource = issueTracker?.eventSource;
final assistantEventSource = issueAssistant?.eventSource;
if (trackerEventSource != null &&
assistantEventSource != null &&
!_eventSourceDocumentsMatch(trackerEventSource, assistantEventSource)) {
throw FormatException(
'projects.$key.issueTracker.eventSource and '
'projects.$key.issueAssistant.eventSource must match when both are set.',
);
}
if (trackerEventSource == null) {
return issueAssistant;
}
return IssueAssistantConfigDocument(
enabled: issueAssistant?.enabled,
eventSource: trackerEventSource,
maxConcurrentIssues: issueAssistant?.maxConcurrentIssues,
mentionTriggers: issueAssistant?.mentionTriggers,
prompt: issueAssistant?.prompt,
responders: issueAssistant?.responders,
capabilities: issueAssistant?.capabilities,
notifications: issueAssistant?.notifications,
commentTag: issueAssistant?.commentTag,
commentPrefixTemplate: issueAssistant?.commentPrefixTemplate,
commentFooterTemplate: issueAssistant?.commentFooterTemplate,
);
}
bool _eventSourceDocumentsMatch(
IssueAssistantEventSourceDocument left,
IssueAssistantEventSourceDocument right,
) {
return left.type == right.type &&
left.pollInterval == right.pollInterval &&
left.reconciliation == right.reconciliation &&
left.reconcileInterval == right.reconcileInterval;
}
class ScmConfig {
const ScmConfig({required this.provider});
@ -422,7 +446,7 @@ class IssueAssistantConfig {
sealed class IssueEventSourceConfig {
const IssueEventSourceConfig({required this.type});
final IssueAssistantEventSourceKind type;
final IssueTrackerEventSourceKind type;
bool get usesChannelEventSource => !_isPollingEventSource(type);
}
@ -436,7 +460,7 @@ class PollingIssueEventSourceConfig extends IssueEventSourceConfig {
final Duration pollInterval;
PollingIssueEventSourceConfig copyWith({
IssueAssistantEventSourceKind? type,
IssueTrackerEventSourceKind? type,
Duration? pollInterval,
}) {
return PollingIssueEventSourceConfig(
@ -453,12 +477,12 @@ class ChannelIssueEventSourceConfig extends IssueEventSourceConfig {
required this.reconcileInterval,
});
final IssueAssistantReconciliationKind reconciliation;
final IssueTrackerReconciliationKind reconciliation;
final Duration reconcileInterval;
ChannelIssueEventSourceConfig copyWith({
IssueAssistantEventSourceKind? type,
IssueAssistantReconciliationKind? reconciliation,
IssueTrackerEventSourceKind? type,
IssueTrackerReconciliationKind? reconciliation,
Duration? reconcileInterval,
}) {
return ChannelIssueEventSourceConfig(
@ -608,7 +632,7 @@ class _IssueAssistantConfigResolver {
required IssueAssistantEventSourceDocument? overlay,
}) {
final type =
overlay?.type ?? base?.type ?? IssueAssistantEventSourceKind.ghPolling;
overlay?.type ?? base?.type ?? IssueTrackerEventSourceKind.ghPolling;
if (_isPollingEventSource(type)) {
if (overlay?.reconciliation != null ||
overlay?.reconcileInterval != null) {
@ -643,7 +667,7 @@ class _IssueAssistantConfigResolver {
switch (base) {
ChannelIssueEventSourceConfig(:final reconciliation) =>
reconciliation,
_ => IssueAssistantReconciliationKind.continuous,
_ => IssueTrackerReconciliationKind.continuous,
},
reconcileInterval: overlay?.reconcileInterval != null
? _parseDuration(overlay!.reconcileInterval!)
@ -654,27 +678,27 @@ class _IssueAssistantConfigResolver {
}
}
String _eventSourceConfigValue(IssueAssistantEventSourceKind value) {
String _eventSourceConfigValue(IssueTrackerEventSourceKind value) {
return switch (value) {
IssueAssistantEventSourceKind.ghPolling => 'gh/polling',
IssueAssistantEventSourceKind.glabPolling => 'glab/polling',
IssueAssistantEventSourceKind.teaPolling => 'tea/polling',
IssueAssistantEventSourceKind.opPolling => 'op/polling',
IssueAssistantEventSourceKind.ghGosmee => 'gh/gosmee',
IssueAssistantEventSourceKind.ghWebhookForward => 'gh/webhook-forward',
IssueAssistantEventSourceKind.teaGosmee => 'tea/gosmee',
IssueTrackerEventSourceKind.ghPolling => 'gh/polling',
IssueTrackerEventSourceKind.glabPolling => 'glab/polling',
IssueTrackerEventSourceKind.teaPolling => 'tea/polling',
IssueTrackerEventSourceKind.opPolling => 'op/polling',
IssueTrackerEventSourceKind.ghGosmee => 'gh/gosmee',
IssueTrackerEventSourceKind.ghWebhookForward => 'gh/webhook-forward',
IssueTrackerEventSourceKind.teaGosmee => 'tea/gosmee',
};
}
bool _isPollingEventSource(IssueAssistantEventSourceKind value) {
bool _isPollingEventSource(IssueTrackerEventSourceKind value) {
return switch (value) {
IssueAssistantEventSourceKind.ghPolling => true,
IssueAssistantEventSourceKind.glabPolling => true,
IssueAssistantEventSourceKind.teaPolling => true,
IssueAssistantEventSourceKind.opPolling => true,
IssueAssistantEventSourceKind.ghGosmee => false,
IssueAssistantEventSourceKind.ghWebhookForward => false,
IssueAssistantEventSourceKind.teaGosmee => false,
IssueTrackerEventSourceKind.ghPolling => true,
IssueTrackerEventSourceKind.glabPolling => true,
IssueTrackerEventSourceKind.teaPolling => true,
IssueTrackerEventSourceKind.opPolling => true,
IssueTrackerEventSourceKind.ghGosmee => false,
IssueTrackerEventSourceKind.ghWebhookForward => false,
IssueTrackerEventSourceKind.teaGosmee => false,
};
}
@ -864,6 +888,13 @@ void _assertCamelCaseConfigKeys(Map<String, dynamic> root) {
issueTracker,
scope: 'projects.${entry.key}.issueTracker',
);
final issueTrackerEventSource = issueTracker['eventSource'];
if (issueTrackerEventSource is Map<String, dynamic>) {
_assertCamelCaseMap(
issueTrackerEventSource,
scope: 'projects.${entry.key}.issueTracker.eventSource',
);
}
}
_assertCamelCaseMap(project, scope: 'projects.${entry.key}');
final scm = project['scm'];
@ -919,6 +950,20 @@ ScmProvider? _defaultScmProviderForTracker(IssueTrackerProvider provider) {
};
}
IssueTrackerProvider _issueTrackerProviderForEventSource(
IssueTrackerEventSourceKind type,
) {
return switch (type) {
IssueTrackerEventSourceKind.ghPolling => IssueTrackerProvider.github,
IssueTrackerEventSourceKind.glabPolling => IssueTrackerProvider.gitlab,
IssueTrackerEventSourceKind.teaPolling => IssueTrackerProvider.gitea,
IssueTrackerEventSourceKind.opPolling => IssueTrackerProvider.openproject,
IssueTrackerEventSourceKind.ghGosmee => IssueTrackerProvider.github,
IssueTrackerEventSourceKind.ghWebhookForward => IssueTrackerProvider.github,
IssueTrackerEventSourceKind.teaGosmee => IssueTrackerProvider.gitea,
};
}
void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
void normalizeProjectTracker(Map<String, dynamic> project) {
final repo = project['repo'];
@ -942,9 +987,31 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
}
}
void normalizeIssueTrackerEventSource(
Map<String, dynamic> issueTracker, {
required IssueTrackerEventSourceKind? defaultType,
}) {
final rawEventSource = issueTracker['eventSource'];
Map<String, dynamic>? normalizedEventSource;
if (rawEventSource is String) {
normalizedEventSource = <String, dynamic>{'type': rawEventSource};
} else if (rawEventSource is Map<String, dynamic>) {
normalizedEventSource = Map<String, dynamic>.from(rawEventSource);
} else {
return;
}
if (defaultType != null && !normalizedEventSource.containsKey('type')) {
normalizedEventSource['type'] = _eventSourceConfigValue(defaultType);
}
issueTracker['eventSource'] = normalizedEventSource;
}
void normalizeIssueAssistant(
Map<String, dynamic> issueAssistant, {
required IssueAssistantEventSourceKind? defaultType,
required IssueTrackerEventSourceKind? defaultType,
}) {
final rawEventSource = issueAssistant['eventSource'];
Map<String, dynamic>? normalizedEventSource;
@ -989,7 +1056,7 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
if (issueAssistant is Map<String, dynamic>) {
normalizeIssueAssistant(
issueAssistant,
defaultType: IssueAssistantEventSourceKind.ghPolling,
defaultType: IssueTrackerEventSourceKind.ghPolling,
);
}
}
@ -1002,8 +1069,28 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
continue;
}
normalizeProjectTracker(project);
final issueTracker = project['issueTracker'];
if (issueTracker is Map<String, dynamic>) {
normalizeIssueTrackerEventSource(
issueTracker,
defaultType: switch (issueTracker['provider'] as String?) {
'gitlab' => IssueTrackerEventSourceKind.glabPolling,
'gitea' => IssueTrackerEventSourceKind.teaPolling,
'openproject' => IssueTrackerEventSourceKind.opPolling,
_ => IssueTrackerEventSourceKind.ghPolling,
},
);
}
final issueAssistant = project['issueAssistant'];
if (issueAssistant is! Map<String, dynamic>) {
if (issueTracker is Map<String, dynamic> &&
issueTracker['eventSource'] is Map<String, dynamic>) {
project['issueAssistant'] = <String, dynamic>{
'eventSource': Map<String, dynamic>.from(
issueTracker['eventSource'] as Map<String, dynamic>,
),
};
}
continue;
}
normalizeIssueAssistant(
@ -1011,11 +1098,19 @@ void _normalizeLegacyEventSourceConfig(Map<String, dynamic> root) {
defaultType: switch ((project['issueTracker']
as Map<String, dynamic>?)?['provider']
as String?) {
'gitlab' => IssueAssistantEventSourceKind.glabPolling,
'gitea' => IssueAssistantEventSourceKind.teaPolling,
_ => IssueAssistantEventSourceKind.ghPolling,
'gitlab' => IssueTrackerEventSourceKind.glabPolling,
'gitea' => IssueTrackerEventSourceKind.teaPolling,
'openproject' => IssueTrackerEventSourceKind.opPolling,
_ => IssueTrackerEventSourceKind.ghPolling,
},
);
if (issueTracker is Map<String, dynamic> &&
issueTracker['eventSource'] == null &&
issueAssistant['eventSource'] is Map<String, dynamic>) {
issueTracker['eventSource'] = Map<String, dynamic>.from(
issueAssistant['eventSource'] as Map<String, dynamic>,
);
}
}
}
}
@ -1098,10 +1193,6 @@ ${_YamlEmitter().write({'projects': projectsSection})}
# issueAssistant:
# enabled: ${assistant?.enabled ?? true}
# eventSource:
# type: gh/gosmee
# reconciliation: startup-only
# reconcileInterval: 30s
# mentionTriggers: ["@helper"]
# sessionPrefix: sample
'''

View File

@ -88,9 +88,10 @@ class ProjectConfigDocument {
includeIfNull: false,
)
class IssueTrackerConfigDocument {
const IssueTrackerConfigDocument({this.provider});
const IssueTrackerConfigDocument({this.provider, this.eventSource});
final IssueTrackerProvider? provider;
final IssueAssistantEventSourceDocument? eventSource;
factory IssueTrackerConfigDocument.fromJson(Map<String, dynamic> json) =>
_$IssueTrackerConfigDocumentFromJson(json);
@ -171,9 +172,9 @@ class IssueAssistantEventSourceDocument {
this.reconcileInterval,
});
final IssueAssistantEventSourceKind? type;
final IssueTrackerEventSourceKind? type;
final String? pollInterval;
final IssueAssistantReconciliationKind? reconciliation;
final IssueTrackerReconciliationKind? reconciliation;
final String? reconcileInterval;
factory IssueAssistantEventSourceDocument.fromJson(
@ -247,7 +248,7 @@ class NotificationConfigDocument {
@JsonEnum(fieldRename: FieldRename.snake)
enum AssistantCapability { planning, bugVerification }
enum IssueAssistantEventSourceKind {
enum IssueTrackerEventSourceKind {
@JsonValue('gh/polling')
ghPolling,
@JsonValue('glab/polling')
@ -264,7 +265,7 @@ enum IssueAssistantEventSourceKind {
teaGosmee,
}
enum IssueAssistantReconciliationKind {
enum IssueTrackerReconciliationKind {
@JsonValue('startup-only')
startupOnly,
@JsonValue('continuous')

View File

@ -63,21 +63,21 @@ class IssueAssistantApp {
issueTrackerClient: issueTrackerClient,
issueEventSource: RoutedIssueEventSource(
sources: {
IssueAssistantEventSourceKind.ghPolling: pollingIssueEventSource,
IssueAssistantEventSourceKind.glabPolling: pollingIssueEventSource,
IssueAssistantEventSourceKind.teaPolling: pollingIssueEventSource,
IssueAssistantEventSourceKind.opPolling: pollingIssueEventSource,
IssueAssistantEventSourceKind.ghGosmee: GitHubGosmeeIssueEventSource(
IssueTrackerEventSourceKind.ghPolling: pollingIssueEventSource,
IssueTrackerEventSourceKind.glabPolling: pollingIssueEventSource,
IssueTrackerEventSourceKind.teaPolling: pollingIssueEventSource,
IssueTrackerEventSourceKind.opPolling: pollingIssueEventSource,
IssueTrackerEventSourceKind.ghGosmee: GitHubGosmeeIssueEventSource(
issueTrackerClient: issueTrackerClient,
gosmeeCommand: gosmeeCommand,
logger: _logger,
),
IssueAssistantEventSourceKind.ghWebhookForward:
IssueTrackerEventSourceKind.ghWebhookForward:
GitHubWebhookForwardIssueEventSource(
issueTrackerClient: issueTrackerClient,
logger: _logger,
),
IssueAssistantEventSourceKind.teaGosmee: GiteaGosmeeIssueEventSource(
IssueTrackerEventSourceKind.teaGosmee: GiteaGosmeeIssueEventSource(
issueTrackerClient: issueTrackerClient,
gosmeeCommand: gosmeeCommand,
logger: _logger,

View File

@ -39,7 +39,7 @@ abstract class IssueEventSource {
class RoutedIssueEventSource implements IssueEventSource {
RoutedIssueEventSource({required this.sources});
final Map<IssueAssistantEventSourceKind, IssueEventSource> sources;
final Map<IssueTrackerEventSourceKind, IssueEventSource> sources;
@override
Future<void> run({
@ -50,17 +50,15 @@ class RoutedIssueEventSource implements IssueEventSource {
.where(_shouldRunStartupReconciliation)
.toList(growable: false);
if (startupReconcileProjects.isNotEmpty) {
await sources[IssueAssistantEventSourceKind.ghPolling]!.runOnce(
await sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
projects: startupReconcileProjects,
onBatch: onBatch,
);
}
final projectsBySource =
<IssueAssistantEventSourceKind, List<ProjectConfig>>{
for (final eventSource in sources.keys)
eventSource: <ProjectConfig>[],
};
final projectsBySource = <IssueTrackerEventSourceKind, List<ProjectConfig>>{
for (final eventSource in sources.keys) eventSource: <ProjectConfig>[],
};
for (final project in projects) {
for (final projectSource in _registeredSourcesFor(project)) {
final sourceProjects = projectsBySource[projectSource];
@ -90,7 +88,7 @@ class RoutedIssueEventSource implements IssueEventSource {
required List<ProjectConfig> projects,
required IssueEventBatchHandler onBatch,
}) {
return sources[IssueAssistantEventSourceKind.ghPolling]!.runOnce(
return sources[IssueTrackerEventSourceKind.ghPolling]!.runOnce(
projects: projects,
onBatch: onBatch,
);
@ -103,33 +101,33 @@ class RoutedIssueEventSource implements IssueEventSource {
}
}
Iterable<IssueAssistantEventSourceKind> _registeredSourcesFor(
Iterable<IssueTrackerEventSourceKind> _registeredSourcesFor(
ProjectConfig project,
) sync* {
switch (project.issueAssistant.eventSource.type) {
case IssueAssistantEventSourceKind.ghPolling:
yield IssueAssistantEventSourceKind.ghPolling;
case IssueAssistantEventSourceKind.glabPolling:
yield IssueAssistantEventSourceKind.glabPolling;
case IssueAssistantEventSourceKind.teaPolling:
yield IssueAssistantEventSourceKind.teaPolling;
case IssueAssistantEventSourceKind.opPolling:
yield IssueAssistantEventSourceKind.opPolling;
case IssueAssistantEventSourceKind.ghGosmee:
case IssueTrackerEventSourceKind.ghPolling:
yield IssueTrackerEventSourceKind.ghPolling;
case IssueTrackerEventSourceKind.glabPolling:
yield IssueTrackerEventSourceKind.glabPolling;
case IssueTrackerEventSourceKind.teaPolling:
yield IssueTrackerEventSourceKind.teaPolling;
case IssueTrackerEventSourceKind.opPolling:
yield IssueTrackerEventSourceKind.opPolling;
case IssueTrackerEventSourceKind.ghGosmee:
if (_shouldRunContinuousReconciliation(project)) {
yield IssueAssistantEventSourceKind.ghPolling;
yield IssueTrackerEventSourceKind.ghPolling;
}
yield IssueAssistantEventSourceKind.ghGosmee;
case IssueAssistantEventSourceKind.ghWebhookForward:
yield IssueTrackerEventSourceKind.ghGosmee;
case IssueTrackerEventSourceKind.ghWebhookForward:
if (_shouldRunContinuousReconciliation(project)) {
yield IssueAssistantEventSourceKind.ghPolling;
yield IssueTrackerEventSourceKind.ghPolling;
}
yield IssueAssistantEventSourceKind.ghWebhookForward;
case IssueAssistantEventSourceKind.teaGosmee:
yield IssueTrackerEventSourceKind.ghWebhookForward;
case IssueTrackerEventSourceKind.teaGosmee:
if (_shouldRunContinuousReconciliation(project)) {
yield IssueAssistantEventSourceKind.teaPolling;
yield IssueTrackerEventSourceKind.teaPolling;
}
yield IssueAssistantEventSourceKind.teaGosmee;
yield IssueTrackerEventSourceKind.teaGosmee;
}
}
@ -141,7 +139,7 @@ class RoutedIssueEventSource implements IssueEventSource {
project.issueAssistant.eventSource is ChannelIssueEventSourceConfig &&
(project.issueAssistant.eventSource as ChannelIssueEventSourceConfig)
.reconciliation ==
IssueAssistantReconciliationKind.continuous;
IssueTrackerReconciliationKind.continuous;
}
class PollingIssueEventSource implements IssueEventSource {

View File

@ -407,21 +407,22 @@ projects:
});
/// ```gherkin
/// Scenario: Load an explicit Gitea issue tracker provider
/// Given a config whose project sets issueTracker.provider to gitea
/// Scenario: Infer a Gitea issue tracker provider from the event source
/// Given a config whose project sets tea/polling as the event source type
/// When loading the app config from disk
/// Then the project keeps the configured Gitea provider
/// Then the project infers the Gitea provider
/// And the repository slug still parses normally
/// ```
test('Load an explicit Gitea issue tracker provider', () async {
// Given a config whose project sets issueTracker.provider to gitea.
test('Infer a Gitea issue tracker provider from the event source', () async {
// Given a config whose project sets tea/polling as the event source type.
final tempDir = await Directory.systemTemp.createTemp('cws-gitea-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
projects:
sample:
issueTracker:
provider: gitea
eventSource:
type: tea/polling
repo: owner/sample
path: ${tempDir.path}
''');
@ -429,7 +430,7 @@ projects:
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the project keeps the configured Gitea provider.
// Then the project infers the Gitea provider.
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitea);
expect(config.projects['sample']!.scm.provider, ScmProvider.gitea);
@ -437,26 +438,27 @@ projects:
expect(config.projects['sample']!.repoSlug!.fullName, 'owner/sample');
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueAssistantEventSourceKind.teaPolling,
IssueTrackerEventSourceKind.teaPolling,
);
});
/// ```gherkin
/// Scenario: Load an explicit GitLab issue tracker provider
/// Given a config whose project sets issueTracker.provider to gitlab
/// Scenario: Infer a GitLab issue tracker provider from the event source
/// Given a config whose project sets glab/polling as the event source type
/// When loading the app config from disk
/// Then the project keeps the configured GitLab provider
/// Then the project infers the GitLab provider
/// And the project uses GitLab polling and GitLab SCM by default
/// ```
test('Load an explicit GitLab issue tracker provider', () async {
// Given a config whose project sets issueTracker.provider to gitlab.
test('Infer a GitLab issue tracker provider from the event source', () async {
// Given a config whose project sets glab/polling as the event source type.
final tempDir = await Directory.systemTemp.createTemp('cws-gitlab-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
projects:
sample:
issueTracker:
provider: gitlab
eventSource:
type: glab/polling
repo: group/subgroup/sample
path: ${tempDir.path}
''');
@ -464,67 +466,75 @@ projects:
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the project keeps the configured GitLab provider.
// Then the project infers the GitLab provider.
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitlab);
// And the project uses GitLab polling and GitLab SCM by default.
expect(config.projects['sample']!.scm.provider, ScmProvider.gitlab);
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueAssistantEventSourceKind.glabPolling,
IssueTrackerEventSourceKind.glabPolling,
);
expect(config.projects['sample']!.repo, 'group/subgroup/sample');
});
/// ```gherkin
/// Scenario: Load an explicit OpenProject issue tracker provider
/// Given a config whose project sets issueTracker.provider to openproject
/// Scenario: Infer an OpenProject issue tracker provider from the event source
/// Given a config whose project sets op/polling as the event source type
/// And the project declares an explicit SCM provider
/// When loading the app config from disk
/// Then the project keeps the configured OpenProject provider
/// Then the project infers the OpenProject provider
/// And the project uses op/polling as its default issue event source
/// ```
test('Load an explicit OpenProject issue tracker provider', () async {
// Given a config whose project sets issueTracker.provider to openproject.
final tempDir = await Directory.systemTemp.createTemp('cws-openproject-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
test(
'Infer an OpenProject issue tracker provider from the event source',
() async {
// Given a config whose project sets op/polling as the event source type.
final tempDir = await Directory.systemTemp.createTemp(
'cws-openproject-',
);
final configFile = File(
p.join(tempDir.path, 'agent-orchestrator.yaml'),
);
await configFile.writeAsString('''
projects:
sample:
issueTracker:
provider: openproject
eventSource:
type: op/polling
scm:
provider: github
repo: 42
path: ${tempDir.path}
''');
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the project keeps the configured OpenProject provider.
expect(
config.projects['sample']!.provider,
IssueTrackerProvider.openproject,
);
expect(config.projects['sample']!.scm.provider, ScmProvider.github);
// Then the project infers the OpenProject provider.
expect(
config.projects['sample']!.provider,
IssueTrackerProvider.openproject,
);
expect(config.projects['sample']!.scm.provider, ScmProvider.github);
// And the project uses op/polling as its default issue event source.
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueAssistantEventSourceKind.opPolling,
);
});
// And the project uses op/polling as its default issue event source.
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueTrackerEventSourceKind.opPolling,
);
},
);
/// ```gherkin
/// Scenario: Require an explicit SCM provider for OpenProject projects
/// Given a config whose project uses the OpenProject issue tracker
/// Given a config whose project uses op/polling as the event source
/// And the project omits scm.provider
/// When loading the app config from disk
/// Then loading fails with a clear SCM requirement error
/// ```
test('Require an explicit SCM provider for OpenProject projects', () async {
// Given a config whose project uses the OpenProject issue tracker.
// Given a config whose project uses op/polling as the event source.
final tempDir = await Directory.systemTemp.createTemp(
'cws-openproject-missing-scm-',
);
@ -533,7 +543,8 @@ projects:
projects:
sample:
issueTracker:
provider: openproject
eventSource:
type: op/polling
repo: 42
path: ${tempDir.path}
''');
@ -551,7 +562,7 @@ projects:
(error) => error.message,
'message',
contains(
'projects.sample.scm.provider is required for issueTracker.provider: openproject',
'projects.sample.scm.provider is required for issue tracking provider: openproject',
),
),
),
@ -560,13 +571,13 @@ projects:
/// ```gherkin
/// Scenario: Accept an OpenProject project name reference
/// Given a config whose project uses the OpenProject issue tracker
/// Given a config whose project uses op/polling as the event source
/// And the project repo field is a human-readable project name
/// When loading the app config from disk
/// Then loading succeeds without requiring a numeric project id
/// ```
test('Accept an OpenProject project name reference', () async {
// Given a config whose project uses the OpenProject issue tracker.
// Given a config whose project uses op/polling as the event source.
final tempDir = await Directory.systemTemp.createTemp(
'cws-openproject-project-name-',
);
@ -575,7 +586,8 @@ projects:
projects:
sample:
issueTracker:
provider: openproject
eventSource:
type: op/polling
scm:
provider: github
repo: Demo project
@ -610,12 +622,10 @@ projects:
projects:
sample:
issueTracker:
provider: github
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: gh/gosmee
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
@ -624,7 +634,7 @@ projects:
// Then the project keeps gh/gosmee as its event source.
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueAssistantEventSourceKind.ghGosmee,
IssueTrackerEventSourceKind.ghGosmee,
);
// And the GitHub provider remains valid for that project.
@ -652,7 +662,8 @@ projects:
projects:
sample:
issueTracker:
provider: gitea
eventSource:
type: tea/polling
scm:
provider: github
repo: owner/sample
@ -671,15 +682,17 @@ projects:
);
/// ```gherkin
/// Scenario: Reject gh/gosmee for non-GitHub providers
/// Given a Gitea project config that opts into gh/gosmee
/// Scenario: Keep the legacy issue tracker provider as a fallback
/// Given a config whose project still sets issueTracker.provider to gitea
/// And the project omits issueAssistant.eventSource.type
/// When loading the app config from disk
/// Then loading fails with a clear provider compatibility error
/// Then the project keeps the configured Gitea provider
/// And the polling event source is rewritten to tea/polling
/// ```
test('Reject gh/gosmee for non-GitHub providers', () async {
// Given a Gitea project config that opts into gh/gosmee.
test('Keep the legacy issue tracker provider as a fallback', () async {
// Given a config whose project still sets issueTracker.provider to gitea.
final tempDir = await Directory.systemTemp.createTemp(
'cws-gh-gosmee-gitea-',
'cws-legacy-provider-fallback-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
@ -689,22 +702,60 @@ projects:
provider: gitea
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
''');
// And the project omits issueAssistant.eventSource.type.
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the project keeps the configured Gitea provider.
expect(config.projects['sample']!.provider, IssueTrackerProvider.gitea);
// And the polling event source is rewritten to tea/polling.
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueTrackerEventSourceKind.teaPolling,
);
});
/// ```gherkin
/// Scenario: Reject conflicting legacy tracker and event source settings
/// Given a project config whose legacy tracker provider is gitea
/// And the project opts into gh/gosmee
/// When loading the app config from disk
/// Then loading fails with a clear conflict error
/// ```
test('Reject conflicting legacy tracker and event source settings', () async {
// Given a project config whose legacy tracker provider is gitea.
final tempDir = await Directory.systemTemp.createTemp(
'cws-gh-gosmee-gitea-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
projects:
sample:
issueTracker:
provider: gitea
eventSource:
type: gh/gosmee
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// Then loading fails with a clear provider compatibility error.
// Then loading fails with a clear conflict error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('gh/gosmee requires issueTracker.provider: github'),
contains(
'gh/gosmee implies issue tracker provider github, but the legacy configured provider is gitea',
),
),
),
);
@ -727,12 +778,10 @@ projects:
projects:
sample:
issueTracker:
provider: github
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: gh/webhook-forward
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
@ -741,7 +790,7 @@ projects:
// Then the project keeps gh/webhook-forward as its event source.
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueAssistantEventSourceKind.ghWebhookForward,
IssueTrackerEventSourceKind.ghWebhookForward,
);
// And the GitHub provider remains valid for that project.
@ -765,14 +814,12 @@ projects:
projects:
sample:
issueTracker:
provider: github
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: gh/webhook-forward
reconciliation: startup-only
reconcileInterval: 5m
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
@ -783,7 +830,7 @@ projects:
(config.projects['sample']!.issueAssistant.eventSource
as ChannelIssueEventSourceConfig)
.reconciliation,
IssueAssistantReconciliationKind.startupOnly,
IssueTrackerReconciliationKind.startupOnly,
);
// And the configured reconcile interval is parsed for channel recovery.
@ -812,13 +859,11 @@ projects:
projects:
sample:
issueTracker:
provider: github
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: gh/webhook-forward
pollInterval: 45s
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
@ -840,46 +885,51 @@ projects:
});
/// ```gherkin
/// Scenario: Reject gh/webhook-forward for non-GitHub providers
/// Given a Gitea project config that opts into gh/webhook-forward
/// Scenario: Reject a legacy Gitea provider that conflicts with gh/webhook-forward
/// Given a project config whose legacy tracker provider is gitea
/// And the project opts into gh/webhook-forward
/// When loading the app config from disk
/// Then loading fails with a clear provider compatibility error
/// Then loading fails with a clear conflict error
/// ```
test('Reject gh/webhook-forward for non-GitHub providers', () async {
// Given a Gitea project config that opts into gh/webhook-forward.
final tempDir = await Directory.systemTemp.createTemp(
'cws-webhook-forward-gitea-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
test(
'Reject a legacy Gitea provider that conflicts with gh/webhook-forward',
() async {
// Given a project config whose legacy tracker provider is gitea.
final tempDir = await Directory.systemTemp.createTemp(
'cws-webhook-forward-gitea-',
);
final configFile = File(
p.join(tempDir.path, 'agent-orchestrator.yaml'),
);
await configFile.writeAsString('''
projects:
sample:
issueTracker:
provider: gitea
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: gh/webhook-forward
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// Then loading fails with a clear provider compatibility error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains(
'gh/webhook-forward requires issueTracker.provider: github',
// Then loading fails with a clear conflict error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains(
'gh/webhook-forward implies issue tracker provider github, but the legacy configured provider is gitea',
),
),
),
),
);
});
);
},
);
/// ```gherkin
/// Scenario: Load tea/gosmee as the issue event source
@ -896,12 +946,10 @@ projects:
projects:
sample:
issueTracker:
provider: gitea
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: tea/gosmee
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
@ -910,7 +958,7 @@ projects:
// Then the project keeps tea/gosmee as its event source.
expect(
config.projects['sample']!.issueAssistant.eventSource.type,
IssueAssistantEventSourceKind.teaGosmee,
IssueTrackerEventSourceKind.teaGosmee,
);
// And the Gitea provider remains valid for that project.
@ -918,44 +966,51 @@ projects:
});
/// ```gherkin
/// Scenario: Reject tea/gosmee for non-Gitea providers
/// Given a GitHub project config that opts into tea/gosmee
/// Scenario: Reject a legacy GitHub provider that conflicts with tea/gosmee
/// Given a project config whose legacy tracker provider is github
/// And the project opts into tea/gosmee
/// When loading the app config from disk
/// Then loading fails with a clear provider compatibility error
/// Then loading fails with a clear conflict error
/// ```
test('Reject tea/gosmee for non-Gitea providers', () async {
// Given a GitHub project config that opts into tea/gosmee.
final tempDir = await Directory.systemTemp.createTemp(
'cws-gosmee-github-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
test(
'Reject a legacy GitHub provider that conflicts with tea/gosmee',
() async {
// Given a project config whose legacy tracker provider is github.
final tempDir = await Directory.systemTemp.createTemp(
'cws-gosmee-github-',
);
final configFile = File(
p.join(tempDir.path, 'agent-orchestrator.yaml'),
);
await configFile.writeAsString('''
projects:
sample:
issueTracker:
provider: github
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
eventSource:
type: tea/gosmee
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// Then loading fails with a clear provider compatibility error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('tea/gosmee requires issueTracker.provider: gitea'),
// Then loading fails with a clear conflict error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains(
'tea/gosmee implies issue tracker provider gitea, but the legacy configured provider is github',
),
),
),
),
);
});
);
},
);
/// ```gherkin
/// Scenario: Reject snake_case config keys
@ -1097,16 +1152,6 @@ projects:
.replaceFirst(' # timeout: 10m', ' timeout: 10m')
.replaceFirst(' # issueAssistant:', ' issueAssistant:')
.replaceFirst(' # enabled: true', ' enabled: true')
.replaceFirst(' # eventSource:', ' eventSource:')
.replaceFirst(' # type: gh/gosmee', ' type: gh/gosmee')
.replaceFirst(
' # reconciliation: startup-only',
' reconciliation: startup-only',
)
.replaceFirst(
' # reconcileInterval: 30s',
' reconcileInterval: 30s',
)
.replaceFirst(
' # mentionTriggers: ["@helper"]',
' mentionTriggers: ["@helper"]',
@ -1138,10 +1183,8 @@ projects:
'@helper',
]);
expect(
(config.projects['sample']!.issueAssistant.eventSource
as ChannelIssueEventSourceConfig)
.reconciliation,
IssueAssistantReconciliationKind.startupOnly,
config.projects['sample']!.issueAssistant.eventSource,
isA<PollingIssueEventSourceConfig>(),
);
});
});

View File

@ -25,16 +25,16 @@ void main() {
final webhookForward = _RecordingIssueEventSource();
final routed = RoutedIssueEventSource(
sources: {
IssueAssistantEventSourceKind.ghPolling: polling,
IssueAssistantEventSourceKind.teaPolling: polling,
IssueAssistantEventSourceKind.ghWebhookForward: webhookForward,
IssueTrackerEventSourceKind.ghPolling: polling,
IssueTrackerEventSourceKind.teaPolling: polling,
IssueTrackerEventSourceKind.ghWebhookForward: webhookForward,
},
);
// And a project that uses gh/webhook-forward with startup-only reconciliation.
final project = _project(
eventSource: IssueAssistantEventSourceKind.ghWebhookForward,
reconciliation: IssueAssistantReconciliationKind.startupOnly,
eventSource: IssueTrackerEventSourceKind.ghWebhookForward,
reconciliation: IssueTrackerReconciliationKind.startupOnly,
);
// When the routed event source starts in long-running mode.
@ -64,16 +64,16 @@ void main() {
final webhookForward = _RecordingIssueEventSource();
final routed = RoutedIssueEventSource(
sources: {
IssueAssistantEventSourceKind.ghPolling: polling,
IssueAssistantEventSourceKind.teaPolling: polling,
IssueAssistantEventSourceKind.ghWebhookForward: webhookForward,
IssueTrackerEventSourceKind.ghPolling: polling,
IssueTrackerEventSourceKind.teaPolling: polling,
IssueTrackerEventSourceKind.ghWebhookForward: webhookForward,
},
);
// And a project that uses gh/webhook-forward with continuous reconciliation.
final project = _project(
eventSource: IssueAssistantEventSourceKind.ghWebhookForward,
reconciliation: IssueAssistantReconciliationKind.continuous,
eventSource: IssueTrackerEventSourceKind.ghWebhookForward,
reconciliation: IssueTrackerReconciliationKind.continuous,
);
// When the routed event source starts in long-running mode.
@ -115,8 +115,8 @@ class _RecordingIssueEventSource implements IssueEventSource {
}
ProjectConfig _project({
required IssueAssistantEventSourceKind eventSource,
required IssueAssistantReconciliationKind reconciliation,
required IssueTrackerEventSourceKind eventSource,
required IssueTrackerReconciliationKind reconciliation,
}) {
return ProjectConfig(
key: 'sample',

View File

@ -78,7 +78,7 @@ void main() {
issueAssistant: IssueAssistantConfig(
enabled: true,
eventSource: const PollingIssueEventSourceConfig(
type: IssueAssistantEventSourceKind.ghPolling,
type: IssueTrackerEventSourceKind.ghPolling,
pollInterval: Duration(seconds: 30),
),
maxConcurrentIssues: 3,
@ -131,7 +131,7 @@ void main() {
issueAssistant: IssueAssistantConfig(
enabled: true,
eventSource: const PollingIssueEventSourceConfig(
type: IssueAssistantEventSourceKind.glabPolling,
type: IssueTrackerEventSourceKind.glabPolling,
pollInterval: Duration(seconds: 30),
),
maxConcurrentIssues: 3,