- 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
320 lines
10 KiB
Dart
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);
|
|
}
|
|
});
|
|
});
|
|
}
|