oto/apps/runner/test/oto_agent_bootstrap_script_test.dart
toki 590e3219da feat: cross-os runner bootstrap task and related updates
- Add agent-task/m-cross-os-runner-bootstrap with subtasks (G07, G06)
- Update roadmap phases and milestones for control-plane-product-surface
- Add runner.proto definitions and regenerate code
- Update HTTP server handler for runner management
- Add smoke tests and update local test configurations
- Update README with latest project status
2026-06-15 17:12:35 +09:00

320 lines
10 KiB
Dart

import 'dart:io';
import 'package:test/test.dart';
const bootstrapAssetMatrix = <String, List<String>>{
'linux': ['oto-linux-x64.tar.gz', 'oto-linux-arm64.tar.gz'],
'macos': ['oto-macos-x64.tar.gz', 'oto-macos-arm64.tar.gz'],
'windows': ['oto-windows-x64.zip', 'oto-windows-arm64.zip'],
};
Iterable<String> allSupportedBootstrapAssets() sync* {
for (final assets in bootstrapAssetMatrix.values) {
yield* assets;
}
}
void main() {
final scriptFile = File('assets/script/shell/oto_agent_bootstrap.sh');
final rootReadmeFile = File('../../README.md');
group('Linux Bootstrap Script Contract Tests', () {
late String content;
setUpAll(() async {
expect(
await scriptFile.exists(),
isTrue,
reason: 'Bootstrap script file must exist',
);
content = await scriptFile.readAsString();
});
test('should contain required flags', () {
expect(content, contains('--server-url'));
expect(content, contains('--agent-id'));
expect(content, contains('--enrollment-token'));
expect(content, contains('--release-base-url'));
});
test('should contain optional/default flags and paths', () {
expect(content, contains('--agent-alias'));
expect(content, contains('--install-dir'));
expect(content, contains('--config-path'));
expect(content, contains('--workspace-root'));
expect(content, contains('--log-dir'));
expect(content, contains('--no-background'));
expect(content, contains('.oto/bin'));
expect(content, contains('.oto/agent/config.yaml'));
});
test('should support correct architecture assets', () {
for (final asset in bootstrapAssetMatrix['linux']!) {
expect(content, contains(asset));
}
});
test(
'should keep README bootstrap asset matrix aligned with test fixture',
() async {
expect(
await rootReadmeFile.exists(),
isTrue,
reason: 'Root README must document bootstrap asset names',
);
final readmeContent = await rootReadmeFile.readAsString();
for (final asset in allSupportedBootstrapAssets()) {
expect(readmeContent, contains(asset));
}
},
);
test('should write expected keys to configuration yaml', () {
expect(content, contains('agent:'));
expect(content, contains('id:'));
expect(content, contains('alias:'));
expect(content, contains('enrollment_token:'));
expect(content, contains('server:'));
expect(content, contains('url:'));
expect(content, contains('runtime:'));
expect(content, contains('install_dir:'));
expect(content, contains('workspace_root:'));
expect(content, contains('log_dir:'));
});
test('should contain agent runtime commands and background behavior', () {
expect(content, contains('agent run'));
expect(content, contains('--config'));
expect(content, contains('nohup'));
expect(content, contains('oto-agent.pid'));
});
});
group('Linux Bootstrap Script Functional & Regressions Tests', () {
test('should fail when release base URL is not HTTPS', () async {
final result = await Process.run('bash', [
scriptFile.absolute.path,
'--server-url', 'https://server.example.com',
'--agent-id', 'test-agent',
'--enrollment-token', 'secret-token-123',
'--release-base-url', 'http://example.com/release', // HTTP URL
]);
expect(result.exitCode, isNot(0));
expect(
result.stderr.toString(),
contains('--release-base-url must use https://'),
);
expect(
result.stdout.toString(),
isNot(contains('Downloading OTO agent')),
);
// enrollment token이 출력에 노출되지 않는지 확인
expect(result.stdout.toString(), isNot(contains('secret-token-123')));
expect(result.stderr.toString(), isNot(contains('secret-token-123')));
});
test(
'should write PID to standard agent state path when custom config-path is specified',
() async {
final tempDir = await Directory.systemTemp.createTemp(
'oto_bootstrap_test_',
);
try {
final tempHome = Directory('${tempDir.path}/home');
await tempHome.create(recursive: true);
// 1. 가짜 OTO 실행 파일 생성
final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src');
await fakeOtoSource.create(recursive: true);
final fakeOtoFile = File('${fakeOtoSource.path}/oto');
await fakeOtoFile.writeAsString(
'#!/bin/sh\nwhile true; do sleep 1; done\n',
);
await Process.run('chmod', ['+x', fakeOtoFile.path]);
// 2. 가짜 tar.gz 아카이브 생성
final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz');
await Process.run('tar', [
'-czf',
fakeTarGz.path,
'-C',
fakeOtoSource.path,
'oto',
]);
// 3. 가짜 curl 스크립트 생성
final fakeBinDir = Directory('${tempDir.path}/bin');
await fakeBinDir.create(recursive: true);
final fakeCurl = File('${fakeBinDir.path}/curl');
await fakeCurl.writeAsString('''#!/bin/sh
out_file=""
while [ \$# -gt 0 ]; do
if [ "\$1" = "-o" ]; then
out_file="\$2"
break
fi
shift
done
if [ -n "\$out_file" ]; then
cp "${fakeTarGz.path}" "\$out_file"
fi
''');
await Process.run('chmod', ['+x', fakeCurl.path]);
// 4. custom config path 지정
final customConfigDir = Directory('${tempDir.path}/custom_config');
await customConfigDir.create(recursive: true);
final customConfigPath = '${customConfigDir.path}/my-config.yaml';
// PATH 환경변수에 fakeBinDir 등록, HOME을 tempHome으로 설정
final env = Map<String, String>.from(Platform.environment);
env['PATH'] = '${fakeBinDir.path}:${env['PATH']}';
env['HOME'] = tempHome.path;
final result = await Process.run('bash', [
scriptFile.absolute.path,
'--server-url',
'https://server.example.com',
'--agent-id',
'test-agent',
'--enrollment-token',
'secret-token-123',
'--release-base-url',
'https://example.com/release',
'--config-path',
customConfigPath,
], environment: env);
expect(
result.exitCode,
0,
reason: 'Script failed with: ${result.stderr}',
);
// 5. 검증
final configFile = File(customConfigPath);
expect(await configFile.exists(), isTrue);
final expectedPidFile = File(
'${tempHome.path}/.oto/agent/oto-agent.pid',
);
expect(
await expectedPidFile.exists(),
isTrue,
reason: 'PID file should exist in standard agent state path',
);
final unexpectedPidFile = File(
'${customConfigDir.path}/oto-agent.pid',
);
expect(
await unexpectedPidFile.exists(),
isFalse,
reason: 'PID file should not exist in custom config path directory',
);
final pidStr = await expectedPidFile.readAsString();
final pid = int.tryParse(pidStr.trim());
if (pid != null) {
Process.killPid(pid);
}
} finally {
await tempDir.delete(recursive: true);
}
},
);
test('should support --edge-url as alias compatibility', () async {
final tempDir = await Directory.systemTemp.createTemp(
'oto_bootstrap_alias_test_',
);
try {
final tempHome = Directory('${tempDir.path}/home');
await tempHome.create(recursive: true);
// 1. 가짜 OTO 실행 파일 생성
final fakeOtoSource = Directory('${tempDir.path}/fake_oto_src');
await fakeOtoSource.create(recursive: true);
final fakeOtoFile = File('${fakeOtoSource.path}/oto');
await fakeOtoFile.writeAsString(
'#!/bin/sh\nwhile true; do sleep 1; done\n',
);
await Process.run('chmod', ['+x', fakeOtoFile.path]);
// 2. 가짜 tar.gz 아카이브 생성
final fakeTarGz = File('${tempDir.path}/oto-linux-x64.tar.gz');
await Process.run('tar', [
'-czf',
fakeTarGz.path,
'-C',
fakeOtoSource.path,
'oto',
]);
// 3. 가짜 curl 스크립트 생성
final fakeBinDir = Directory('${tempDir.path}/bin');
await fakeBinDir.create(recursive: true);
final fakeCurl = File('${fakeBinDir.path}/curl');
await fakeCurl.writeAsString('''#!/bin/sh
out_file=""
while [ \$# -gt 0 ]; do
if [ "\$1" = "-o" ]; then
out_file="\$2"
break
fi
shift
done
if [ -n "\$out_file" ]; then
cp "${fakeTarGz.path}" "\$out_file"
fi
''');
await Process.run('chmod', ['+x', fakeCurl.path]);
final customConfigPath = '${tempDir.path}/alias-config.yaml';
final env = Map<String, String>.from(Platform.environment);
env['PATH'] = '${fakeBinDir.path}:${env['PATH']}';
env['HOME'] = tempHome.path;
final result = await Process.run('bash', [
scriptFile.absolute.path,
'--edge-url',
'https://edge.example.com',
'--agent-id',
'test-agent-alias',
'--enrollment-token',
'secret-token-123',
'--release-base-url',
'https://example.com/release',
'--config-path',
customConfigPath,
], environment: env);
expect(
result.exitCode,
0,
reason: 'Script failed with: \${result.stderr}',
);
final configFile = File(customConfigPath);
expect(await configFile.exists(), isTrue);
final configContent = await configFile.readAsString();
expect(configContent, contains('server:'));
expect(configContent, contains('url: "https://edge.example.com"'));
final expectedPidFile = File(
'${tempHome.path}/.oto/agent/oto-agent.pid',
);
expect(await expectedPidFile.exists(), isTrue);
final pidStr = await expectedPidFile.readAsString();
final pid = int.tryParse(pidStr.trim());
if (pid != null) {
Process.killPid(pid);
}
} finally {
await tempDir.delete(recursive: true);
}
});
});
}