oto/test/oto_core_test.dart

444 lines
13 KiB
Dart

import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_registry.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/pipeline/pipeline.dart';
import 'package:test/test.dart';
void main() {
setUp(() {
Application.instance.property = {};
Application.instance.commandStates = {};
Application.instance.dataCommandMap = {};
});
test('parses minimal build yaml into DataBuild', () {
const yaml = '''
property:
workspace: .
commands:
- command: Print
id: hello
param: hi
pipeline:
id: main
workflow:
- exe: hello
''';
final map = Application.getMapFromYamlA(yaml);
expect(map, isNotNull);
final build = DataBuild.fromJson(map!);
expect(build.property!['workspace'], '.');
expect(build.commands, hasLength(1));
expect(build.commands.first.id, 'hello');
expect(build.commands.first.command, CommandType.Print);
expect(build.pipeline!.id, 'main');
expect(build.pipeline!.workflow, hasLength(1));
});
test('tag system preserves single tag object values', () {
Application.instance.property['items'] = [1, 2, 3];
Application.instance.property['config'] = {'a': 1};
final listValue = Command.replaceTagValue('<!property.items>');
expect(listValue, isA<List>());
expect(listValue, equals([1, 2, 3]));
final mapValue = Command.replaceTagValue('<!property.config>');
expect(mapValue, isA<Map>());
expect(mapValue, equals({'a': 1}));
final embedded = Command.replaceTagValue('values=<!property.items>');
expect(embedded, isA<String>());
expect(embedded, contains('1'));
});
test('pipeline validation rejects missing command id', () {
final result = Pipeline.pipelineInitialize([
{'exe': 'doesNotExist'}
]);
expect(result.enable, isFalse);
expect(result.message, contains('doesNotExist'));
});
// ── Explicit context regression tests ──────────────────────────────────────
// Each test puts a different value in the singleton vs. the explicit context
// and asserts that flow-control uses the explicit context exclusively.
ExecutionContext makeCtx(Map<String, dynamic> props,
List<String> commandIds) {
registerAllCommands();
final ctx = ExecutionContext();
ctx.property.addAll(props);
for (final id in commandIds) {
final cmd = DataCommand()
..command = CommandType.Print
..id = id
..param = {'message': id};
ctx.dataCommandMap[id] = cmd;
}
return ctx;
}
test('PipelineIf evaluates condition from explicit context not singleton',
() async {
// Singleton: env = 'dev' → on-false; explicit: env = 'prod' → on-true
Application.instance.property['env'] = 'dev';
final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']);
final result = Pipeline.pipelineInitialize([
{
'if': {
'condition-string': '<!property.env> == prod',
'on-true': [
{'exe': 'onTrue'}
],
'on-false': [
{'exe': 'onFalse'}
],
}
}
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(ctx.commandStates['onTrue'], CommandState.complete);
expect(ctx.commandStates['onFalse'], CommandState.ready);
});
test('PipelineForeach iterates list from explicit context not singleton',
() async {
// Singleton: items = [] (empty); explicit: items = [10, 20, 30]
Application.instance.property['items'] = <dynamic>[];
final ctx = makeCtx({'items': [10, 20, 30]}, ['print']);
final result = Pipeline.pipelineInitialize([
{
'foreach': {
'iterator': '<!property.items>',
'setValue': '<@property.current>',
'on-do': [
{'exe': 'print'}
],
}
}
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
// Loop ran 3 times using explicit context; last assigned value = 30
expect(ctx.property['current'], 30);
// Singleton must not have been modified
expect(Application.instance.property.containsKey('current'), isFalse);
});
test('PipelineContain evaluates target from explicit context not singleton',
() async {
// Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list)
Application.instance.property['target'] = 'xyz';
final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']);
final result = Pipeline.pipelineInitialize([
{
'contain': {
'target': '<!property.target>',
'list': ['foo', 'bar'],
'on-true': [
{'exe': 'onTrue'}
],
'on-false': [
{'exe': 'onFalse'}
],
}
}
], context: ctx);
expect(result.enable, isTrue, reason: result.message);
await result.pipeline!.execute();
expect(ctx.commandStates['onTrue'], CommandState.complete);
expect(ctx.commandStates['onFalse'], CommandState.ready);
});
// REVIEW_VALIDATE-2: malformed nested pipeline regression tests
test('pipeline validation rejects if with no condition field', () {
final result = Pipeline.pipelineInitialize([
{
'if': {'on-true': []}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNotNull);
expect(result.message, isNot(contains('is not a subtype')));
});
test('pipeline validation rejects foreach with non-string iterator', () {
final result = Pipeline.pipelineInitialize([
{
'foreach': {
'iterator': 123,
'setValue': '<@property.item>',
'on-do': [
{'exe': 'someCmd'}
],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNotNull);
expect(result.message, contains('foreach'));
expect(result.message, isNot(contains('is not a subtype')));
});
test('pipeline validation rejects switch with non-list cases', () {
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'test',
'cases': 'not-a-list',
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNotNull);
expect(result.message, contains('switch'));
expect(result.message, isNot(contains('is not a subtype')));
});
// REVIEW_REVIEW_VALIDATE-3: branch list field validation regression tests
test('pipeline validation rejects exe-handle with non-list on-success', () {
final result = Pipeline.pipelineInitialize([
{
'exe-handle': {
'id': 'hello',
'on-success': 'not-a-list',
'on-fail': [{'exe': 'hello'}],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('exe-handle'));
expect(result.message, contains('on-success'));
});
test('pipeline validation rejects exe-handle with non-list on-fail', () {
final result = Pipeline.pipelineInitialize([
{
'exe-handle': {
'id': 'hello',
'on-success': [{'exe': 'hello'}],
'on-fail': 'not-a-list',
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('exe-handle'));
expect(result.message, contains('on-fail'));
});
test('pipeline validation rejects if with non-list on-true', () {
final result = Pipeline.pipelineInitialize([
{
'if': {
'condition-string': 'a == b',
'on-true': 'not-a-list',
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('if'));
expect(result.message, contains('on-true'));
});
test('pipeline validation rejects if with non-list on-false', () {
final result = Pipeline.pipelineInitialize([
{
'if': {
'condition-string': 'a == b',
'on-true': [{'exe': 'hello'}],
'on-false': 'not-a-list',
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('if'));
expect(result.message, contains('on-false'));
});
test('pipeline validation rejects while with non-list on-do', () {
final result = Pipeline.pipelineInitialize([
{
'while': {
'condition-bool': 'true',
'on-do': 'not-a-list',
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('while'));
expect(result.message, contains('on-do'));
});
test('pipeline validation rejects foreach with non-list on-do', () {
final result = Pipeline.pipelineInitialize([
{
'foreach': {
'iterator': '<!property.items>',
'setValue': '<@property.item>',
'on-do': 'not-a-list',
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('foreach'));
expect(result.message, contains('on-do'));
});
test('pipeline validation rejects switch with non-map case element', () {
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'prod',
'cases': ['not-a-map'],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('switch'));
expect(result.message, contains('cases'));
});
test('pipeline validation rejects switch with missing tasks in case', () {
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'prod',
'cases': [
{'value': 'prod'}
],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('switch'));
expect(result.message, contains('tasks'));
});
test('pipeline validation rejects switch with non-list tasks in case', () {
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'prod',
'cases': [
{'value': 'prod', 'tasks': 'not-a-list'}
],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('switch'));
expect(result.message, contains('tasks'));
});
// REVIEW_REVIEW_VALIDATE-2: contain node guard regression tests
test('pipeline validation rejects contain with non-map value', () {
final result = Pipeline.pipelineInitialize([
{'contain': 123}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('contain'));
});
test('pipeline validation rejects contain with missing target', () {
final result = Pipeline.pipelineInitialize([
{
'contain': {
'list': ['a', 'b'],
'on-true': [{'exe': 'x'}],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('contain'));
expect(result.message, contains('target'));
});
test('pipeline validation rejects contain with non-string target', () {
final result = Pipeline.pipelineInitialize([
{
'contain': {
'target': 123,
'list': ['a', 'b'],
'on-true': [{'exe': 'x'}],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('contain'));
expect(result.message, contains('target'));
});
test('pipeline validation rejects contain with missing list', () {
final result = Pipeline.pipelineInitialize([
{
'contain': {
'target': 'foo',
'on-true': [{'exe': 'x'}],
}
}
]);
expect(result.enable, isFalse);
expect(result.message, isNot(contains('is not a subtype')));
expect(result.message, contains('contain'));
expect(result.message, contains('list'));
});
test(
'PipelineSwitch validate uses explicit context command map not singleton',
() {
// Singleton dataCommandMap is empty; only explicit context has the command.
registerAllCommands();
final ctx = ExecutionContext();
final cmd = DataCommand()
..command = CommandType.Print
..id = 'deployProd'
..param = {'message': 'prod'};
ctx.dataCommandMap['deployProd'] = cmd;
final result = Pipeline.pipelineInitialize([
{
'switch': {
'condition-string': 'prod',
'cases': [
{
'value': 'prod',
'tasks': [
{'exe': 'deployProd'}
],
}
],
}
}
], context: ctx);
// Should succeed: deployProd exists in explicit context.
// Would fail if singleton (empty) context were used.
expect(result.enable, isTrue, reason: result.message);
});
}