[03-Developer] #218 Dev round 2 — AC-6 RAM 4GB gate + AC-10 docs cleanup

QA round 1 (commit 9a9eb2a) FAIL 시 누락된 두 AC 보강.

AC-6: device_info_plus 만으론 4GB 임계 측정 불가 (isLowRamDevice 는
~1GB 기준). MethodChannel `life_helper/device_caps` 신설 + MainActivity.kt
에서 ActivityManager.MemoryInfo.totalMem 노출. data/ai/device_capabilities.dart
는 DeviceCapabilities abstract + PlatformDeviceCapabilities + 4 GiB
임계. deviceMeetsAiRamProvider (FutureProvider<bool>, fail-closed).
SettingsScreen 토글 disabled + "RAM 부족" 안내 (RAM < 4GB).

AC-10: docs/reference/215-ai-frame-suggest.md 의 OQ-1/placeholder
6곳을 실 구현 표현으로 갱신. §8 알려진 제약 = AC-6 device gate +
AC-7 실 단말 E2E + F1 unload + #221 corpus 평가. §9 다음 단계 =
#219~#222 follow-up 목록. 신규 테스트 합계 41 / 전체 88 통과.

테스트: device_capabilities_test.dart 7 신규 (kAiMinRamBytes 동등,
null/0/3.9GB/4GB-1/4GB/8GB 경계). flutter analyze 무이슈, 전체 88 통과
(71 기존 + 10 gemma + 7 RAM gate).

Architect 설계서 §4 의 "RAM 4GB 차단 = AC-9 재활용" 문구는 사실 #215
미구현 사항이라 본 라운드에서 신규 추가했음을 README 에 명기.

Refs #218
This commit is contained in:
2026-06-12 15:45:14 +09:00
parent 9a9eb2abd5
commit f71d132fa3
9 changed files with 223 additions and 19 deletions

View File

@@ -0,0 +1,54 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:life_helper/data/ai/device_capabilities.dart';
/// #218 AC-6 boundary tests for the RAM gate.
///
/// We test the abstract contract's `meetsAiMinRam()` default impl via a
/// fake — the real `PlatformDeviceCapabilities.totalRamBytes()` requires
/// the MethodChannel + Android runtime (covered by AC-7).
class _Fake implements DeviceCapabilities {
_Fake(this.bytes);
final int? bytes;
@override
Future<int?> totalRamBytes() async => bytes;
@override
Future<bool> meetsAiMinRam() async {
final v = await totalRamBytes();
if (v == null) return false;
return v >= kAiMinRamBytes;
}
}
void main() {
test('kAiMinRamBytes equals 4 GiB', () {
expect(kAiMinRamBytes, 4 * 1024 * 1024 * 1024);
});
test('null totalRamBytes → meetsAiMinRam false (fail-closed)', () async {
expect(await _Fake(null).meetsAiMinRam(), isFalse);
});
test('3.9 GiB → meetsAiMinRam false', () async {
final bytes = (3.9 * 1024 * 1024 * 1024).round();
expect(await _Fake(bytes).meetsAiMinRam(), isFalse);
});
test('exactly 4 GiB - 1 byte → false', () async {
expect(await _Fake(kAiMinRamBytes - 1).meetsAiMinRam(), isFalse);
});
test('exactly 4 GiB → true (inclusive)', () async {
expect(await _Fake(kAiMinRamBytes).meetsAiMinRam(), isTrue);
});
test('8 GiB → true', () async {
final bytes = 8 * 1024 * 1024 * 1024;
expect(await _Fake(bytes).meetsAiMinRam(), isTrue);
});
test('0 bytes → false (would also catch broken channel returning 0)', () async {
expect(await _Fake(0).meetsAiMinRam(), isFalse);
});
}