fix: infer tracker provider from event source (#38)
This commit is contained in:
parent
f1eea7abd8
commit
da256d068c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,16 +50,14 @@ 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)) {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,35 +466,42 @@ 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'));
|
||||
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
|
||||
|
|
@ -502,7 +511,7 @@ projects:
|
|||
// When loading the app config from disk.
|
||||
final config = await AppConfig.load(configFile.path);
|
||||
|
||||
// Then the project keeps the configured OpenProject provider.
|
||||
// Then the project infers the OpenProject provider.
|
||||
expect(
|
||||
config.projects['sample']!.provider,
|
||||
IssueTrackerProvider.openproject,
|
||||
|
|
@ -512,19 +521,20 @@ projects:
|
|||
// And the project uses op/polling as its default issue event source.
|
||||
expect(
|
||||
config.projects['sample']!.issueAssistant.eventSource.type,
|
||||
IssueAssistantEventSourceKind.opPolling,
|
||||
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,33 +885,37 @@ 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.
|
||||
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'));
|
||||
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);
|
||||
|
||||
// Then loading fails with a clear provider compatibility error.
|
||||
// Then loading fails with a clear conflict error.
|
||||
await expectLater(
|
||||
load,
|
||||
throwsA(
|
||||
|
|
@ -874,12 +923,13 @@ projects:
|
|||
(error) => error.message,
|
||||
'message',
|
||||
contains(
|
||||
'gh/webhook-forward requires issueTracker.provider: github',
|
||||
'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.
|
||||
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'));
|
||||
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);
|
||||
|
||||
// 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('tea/gosmee requires issueTracker.provider: gitea'),
|
||||
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>(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue