code_work_spawner/test/app_config_test.dart

299 lines
10 KiB
Dart

import 'dart:io';
import 'package:code_work_spawner/code_work_spawner.dart';
import 'package:drift/drift.dart' show driftRuntimeOptions;
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
/// ```gherkin
/// Feature: AppConfig loading
///
/// As a user of the Code Work Spawner
/// I want the app configuration to correctly merge defaults with project-specific overrides
/// So that I can specify common settings once and customize only what differs for each project
/// ```
group('AppConfig.load', () {
/// ```gherkin
/// Scenario: Merge defaults with project overrides
/// Given AO style config with shared defaults and one project override
/// When loading the app config from disk
/// Then the project keeps inherited defaults that were not overridden
/// And the project-specific prompt replaces the shared prompt
/// ```
test('Merge defaults with project overrides', () async {
// Given AO style config with shared defaults and one project override.
final tempDir = await Directory.systemTemp.createTemp('cws-config-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
defaults:
issueAssistant:
enabled: true
pollInterval: 5m
mentionTriggers: ["@helper"]
prompt: "default {{repo}}"
commentPrefixTemplate: "> default prefix {{gh_login}}"
commentFooterTemplate: "default footer {{issue_number}}"
responders:
- id: fallback
command: responder
projects:
sample:
repo: owner/sample
path: ${tempDir.path}
defaultBranch: main
issueAssistant:
prompt: "project {{project_key}}"
''');
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the project keeps inherited defaults that were not overridden.
expect(config.projects['sample'], isNotNull);
expect(config.projects['sample']!.repoSlug.owner, 'owner');
expect(config.projects['sample']!.repoSlug.name, 'sample');
expect(config.projects['sample']!.issueAssistant.mentionTriggers, [
'@helper',
]);
expect(
config.projects['sample']!.issueAssistant.responders.single.id,
'fallback',
);
// And the project-specific prompt replaces the shared prompt.
expect(
config.projects['sample']!.issueAssistant.prompt,
'project {{project_key}}',
);
});
/// ```gherkin
/// Scenario: Use exported default automation comment templates
/// Given an orchestrator config that relies on default issue assistant comment templates
/// When loading the app config from disk
/// Then the assistant uses the exported default prefix and footer templates
/// ```
test('Use exported default automation comment templates', () async {
// Given an orchestrator config that relies on default issue assistant comment templates.
final tempDir = await Directory.systemTemp.createTemp(
'cws-default-templates-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
projects:
sample:
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the assistant uses the exported default prefix and footer templates.
expect(
config.projects['sample']!.issueAssistant.commentPrefixTemplate,
defaultCommentPrefixTemplate,
);
expect(
config.projects['sample']!.issueAssistant.commentFooterTemplate,
defaultCommentFooterTemplate,
);
expect(
config.projects['sample']!.issueAssistant.pollInterval,
const Duration(seconds: 30),
);
expect(config.projects['sample']!.issueAssistant.maxConcurrentIssues, 3);
});
/// ```gherkin
/// Scenario: Override visible comment templates per project
/// Given AO style config with shared defaults and project-specific comment templates
/// When loading the app config from disk
/// Then the project-specific comment templates replace the shared defaults
/// ```
test('Override visible comment templates per project', () async {
// Given AO style config with shared defaults and project-specific comment templates.
final tempDir = await Directory.systemTemp.createTemp(
'cws-comment-template-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
defaults:
issueAssistant:
commentPrefixTemplate: "> default prefix"
commentFooterTemplate: "default footer"
projects:
sample:
repo: owner/sample
path: ${tempDir.path}
issueAssistant:
commentPrefixTemplate: ""
commentFooterTemplate: "_project footer_"
''');
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the project-specific comment templates replace the shared defaults.
expect(
config.projects['sample']!.issueAssistant.commentPrefixTemplate,
'',
);
expect(
config.projects['sample']!.issueAssistant.commentFooterTemplate,
'_project footer_',
);
});
/// ```gherkin
/// Scenario: Normalize legacy codex json flag
/// Given an orchestrator config that still uses the legacy codex --json-output flag
/// When loading the app config from disk
/// Then the responder args are normalized to the current codex --json flag
/// ```
test('Normalize legacy codex json flag', () async {
// Given an orchestrator config that still uses the legacy codex --json-output flag.
final tempDir = await Directory.systemTemp.createTemp('cws-codex-args-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
defaults:
issueAssistant:
responders:
- id: codex
command: codex
args: ["exec", "--json-output"]
projects:
sample:
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the responder args are normalized to the current codex --json flag.
expect(config.projects['sample']!.issueAssistant.responders.single.args, [
'exec',
'--json',
]);
});
/// ```gherkin
/// Scenario: Load AO subset config with shared worktree root
/// Given an AO style shortcut config with dataDir, worktreeDir, and project defaults
/// When loading the app config from disk
/// Then the app exposes the configured data and worktree directories
/// And each project inherits the supported shared defaults
/// ```
test('Load AO subset config with shared worktree root', () async {
// Given an AO style shortcut config with dataDir, worktreeDir, and project defaults.
final tempDir = await Directory.systemTemp.createTemp('cws-ao-subset-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
dataDir: ~/.agent-orchestrator
worktreeDir: ~/.worktrees
defaults:
defaultBranch: trunk
sessionPrefix: ao
projects:
sample:
repo: owner/sample
path: ${tempDir.path}
''');
// When loading the app config from disk.
final config = await AppConfig.load(configFile.path);
// Then the app exposes the configured data and worktree directories.
expect(
config.dataDir,
p.join(Platform.environment['HOME']!, '.agent-orchestrator'),
);
expect(
config.worktreeDir,
p.join(Platform.environment['HOME']!, '.worktrees'),
);
// And each project inherits the supported shared defaults.
expect(config.projects['sample'], isNotNull);
expect(config.projects['sample']!.defaultBranch, 'trunk');
expect(config.projects['sample']!.sessionPrefix, 'ao');
expect(config.projects['sample']!.issueAssistant.enabled, isTrue);
});
/// ```gherkin
/// Scenario: Reject unsupported AO fields
/// Given an AO style config that includes an unsupported field
/// When loading the app config from disk
/// Then loading fails with a clear config error
/// ```
test('Reject unsupported AO fields', () async {
// Given an AO style config that includes an unsupported field.
final tempDir = await Directory.systemTemp.createTemp('cws-ao-invalid-');
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
worktreeDir: ~/.worktrees
projects:
sample:
repo: owner/sample
path: ${tempDir.path}
tracker:
plugin: github
''');
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// Then loading fails with a clear config error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('Unsupported config field: projects.sample.tracker'),
),
),
);
});
/// ```gherkin
/// Scenario: Reject malformed repository slugs
/// Given an orchestrator config whose project repo is not in owner/repo format
/// When loading the app config from disk
/// Then loading fails with a clear repository slug error
/// ```
test('Reject malformed repository slugs', () async {
// Given an orchestrator config whose project repo is not in owner/repo format.
final tempDir = await Directory.systemTemp.createTemp(
'cws-invalid-repo-slug-',
);
final configFile = File(p.join(tempDir.path, 'agent-orchestrator.yaml'));
await configFile.writeAsString('''
projects:
sample:
repo: owner
path: ${tempDir.path}
''');
// When loading the app config from disk.
final load = AppConfig.load(configFile.path);
// Then loading fails with a clear repository slug error.
await expectLater(
load,
throwsA(
isA<FormatException>().having(
(error) => error.message,
'message',
contains('Invalid repository slug for projects.sample.repo'),
),
),
);
});
});
}