From ed2c5ace26160b5615ed65841c43a9e49a6189ba Mon Sep 17 00:00:00 2001 From: flemming-it Date: Sun, 12 Jul 2026 23:27:49 +0200 Subject: [PATCH] test: generated nav manifest guards the sidebar truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test/nav_manifest_test.dart derives docs/nav.generated.json from the _pages list (order = Cmd numbers), the labelOf switch, and both .arb files. On any nav change it regenerates the manifest and fails once with instructions to commit + mirror it to fai_chain/docs/studio/, where the platform repo's docs_consistency gate checks the operator guide against it — cross-repo nav drift becomes a red gate instead of quietly rotting docs. Signed-off-by: flemming-it --- docs/nav.generated.json | 53 ++++++++++++++++ test/nav_manifest_test.dart | 118 ++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 docs/nav.generated.json create mode 100644 test/nav_manifest_test.dart diff --git a/docs/nav.generated.json b/docs/nav.generated.json new file mode 100644 index 0000000..0a8ff7f --- /dev/null +++ b/docs/nav.generated.json @@ -0,0 +1,53 @@ +{ + "_generated": "by test/nav_manifest_test.dart — do not edit. Mirror to fai_chain/docs/studio/nav.generated.json (same content).", + "pages": [ + { + "id": "welcome", + "cmd": 1, + "label_de": "Willkommen", + "label_en": "Welcome" + }, + { + "id": "store", + "cmd": 2, + "label_de": "Store", + "label_en": "Store" + }, + { + "id": "doctor", + "cmd": 3, + "label_de": "Diagnose", + "label_en": "Doctor" + }, + { + "id": "flows", + "cmd": 4, + "label_de": "Flows", + "label_en": "Flows" + }, + { + "id": "audit", + "cmd": 5, + "label_de": "Protokoll", + "label_en": "Audit" + }, + { + "id": "approvals", + "cmd": 6, + "label_de": "Freigaben", + "label_en": "Approvals" + }, + { + "id": "runs", + "cmd": 7, + "label_de": "Läufe", + "label_en": "Runs" + }, + { + "id": "federation", + "cmd": 8, + "label_de": "Föderation", + "label_en": "Federation" + } + ] +} diff --git a/test/nav_manifest_test.dart b/test/nav_manifest_test.dart new file mode 100644 index 0000000..1646110 --- /dev/null +++ b/test/nav_manifest_test.dart @@ -0,0 +1,118 @@ +// 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.', + ); + } + }); +}