feat: content repository merges cached seasons with bundled fallback

Extends ContentRepository with optional cacheDir: cached packs in
<cacheDir>/seasons/*/pack.json merge with bundled ones (cached wins
for same id), corrupt/future-schema packs silently ignored, refresh()
fires ContentDownloader.sync() once per session. main() wires the real
cache+downloader instance; default constructor stays bundled-only for tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:15:35 +09:00
parent a820e97237
commit e722fe2ce1
3 changed files with 170 additions and 11 deletions
+75
View File
@@ -1,4 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:block_seasons/data/content_repository.dart';
import 'package:block_seasons/game/models/season.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
@@ -25,4 +29,75 @@ void main() {
final seasons = await repo.availableSeasons();
expect(seasons.map((s) => s.seasonId), contains('season_001'));
});
group('cache merge and fallback', () {
late Directory tmp;
setUp(() async {
tmp = await Directory.systemTemp.createTemp('bs_repo_');
});
tearDown(() async {
if (await tmp.exists()) await tmp.delete(recursive: true);
});
Map<String, dynamic> packJson(String id) => {
'schemaVersion': 1,
'seasonId': id,
'version': 1,
'title': {'en': id},
'theme': const SeasonTheme().toJson(),
'stages': [
{
'id': '${id}_001',
'seed': 1,
'moveLimit': 10,
'preset': [],
'objectives': [
{'type': 'reachScore', 'target': 100}
],
'stars': {
'two': {'movesLeft': 2},
'three': {'movesLeft': 4}
},
'generatorProfile': 'mid',
}
],
};
Future<void> putCachedSeason(String id) async {
final dir = Directory('${tmp.path}/seasons/$id');
await dir.create(recursive: true);
await File('${dir.path}/pack.json')
.writeAsString(jsonEncode(packJson(id)));
}
test('merges cached seasons after bundled ones, sorted by id', () async {
TestWidgetsFlutterBinding.ensureInitialized();
await putCachedSeason('season_002');
final repo = ContentRepository(cacheDir: tmp);
final seasons = await repo.availableSeasons();
expect(seasons.map((s) => s.seasonId).toList(),
['season_001', 'season_002']);
});
test('cached copy of a bundled season wins over the bundle', () async {
TestWidgetsFlutterBinding.ensureInitialized();
await putCachedSeason('season_001');
final repo = ContentRepository(cacheDir: tmp);
final seasons = await repo.availableSeasons();
// Cached fake has exactly 1 stage; the bundled real one has 60.
expect(seasons.single.stages, hasLength(1));
});
test('corrupt cached pack falls back to bundle silently', () async {
TestWidgetsFlutterBinding.ensureInitialized();
final dir = Directory('${tmp.path}/seasons/season_001');
await dir.create(recursive: true);
await File('${dir.path}/pack.json').writeAsString('{not json');
final repo = ContentRepository(cacheDir: tmp);
final seasons = await repo.availableSeasons();
expect(seasons.single.stages, hasLength(60));
});
});
}