// Nav-manifest guard. The sidebar truth lives in three places in // this repo: the `_pages` list (ids + order = Cmd numbers), the // `labelOf` switch (id → l10n key), and the two .arb files (key → // DE/EN label). The operator guide documenting that nav lives in // the PLATFORM repo (fai_chain/docs/studio/operator-guide.md), // which this repo's CI cannot see — so the truth is exported as a // committed manifest, `docs/nav.generated.json`, and the platform // repo's docs_consistency test checks the guide against it. // // This test derives the manifest from the sources. When it differs // from the committed file it REWRITES the file and fails once with // instructions — commit the regenerated manifest and mirror it to // fai_chain/docs/studio/nav.generated.json so the cross-repo guard // stays truthful. Add/rename/reorder a page and forget the guide: // the platform gate goes loudly red instead of the docs quietly // rotting. import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; const _manifestPath = 'docs/nav.generated.json'; const _mirrorHint = 'fai_chain/docs/studio/nav.generated.json (same content)'; /// Extract the ordered page ids from the `_pages` list in main.dart. List _pageIds(String mainDart) { final block = RegExp( r'static const _pages = <_NavPage>\[(.*?)\n \];', dotAll: true, ).firstMatch(mainDart); if (block == null) { fail('could not locate the _pages block in lib/main.dart'); } return RegExp(r"id: '([a-z-]+)'") .allMatches(block.group(1)!) .map((m) => m.group(1)!) .toList(); } /// Extract the id → l10n-key mapping from the `labelOf` switch. Map _labelKeys(String mainDart) { final keys = {}; for (final m in RegExp( r"case '([a-z-]+)':\s*\n\s*return l\.(nav\w+);", ).allMatches(mainDart)) { keys[m.group(1)!] = m.group(2)!; } if (keys.isEmpty) { fail('could not parse the labelOf switch in lib/main.dart'); } return keys; } Map _arb(String path) => jsonDecode(File(path).readAsStringSync()) as Map; void main() { test('docs/nav.generated.json matches the sidebar truth', () { final mainDart = File('lib/main.dart').readAsStringSync(); final ids = _pageIds(mainDart); final labelKeys = _labelKeys(mainDart); final de = _arb('lib/l10n/app_de.arb'); final en = _arb('lib/l10n/app_en.arb'); final pages = >[]; for (var i = 0; i < ids.length; i++) { final key = labelKeys[ids[i]]; expect( key, isNotNull, reason: "page '${ids[i]}' has no labelOf case — add it so the " 'sidebar can localize the label', ); final labelDe = de[key] as String?; final labelEn = en[key] as String?; expect( labelDe, isNotNull, reason: "l10n key '$key' missing from app_de.arb", ); expect( labelEn, isNotNull, reason: "l10n key '$key' missing from app_en.arb", ); pages.add({ 'id': ids[i], 'cmd': i + 1, 'label_de': labelDe, 'label_en': labelEn, }); } const encoder = JsonEncoder.withIndent(' '); final manifest = '${encoder.convert({ '_generated': 'by test/nav_manifest_test.dart — do not edit. ' 'Mirror to $_mirrorHint.', 'pages': pages, })}\n'; final file = File(_manifestPath); final current = file.existsSync() ? file.readAsStringSync() : null; if (current != manifest) { file.parent.createSync(recursive: true); file.writeAsStringSync(manifest); fail( 'the sidebar nav changed: $_manifestPath was regenerated. ' 'Commit it, mirror it to $_mirrorHint, and update the ' 'operator guide — the platform repo\'s docs_consistency ' 'test checks the guide against this manifest.', ); } }); }