Files
hash-wallet/test/brand_assets_match_source_test.dart
T
jwinterm fff5259860 fix(brand): de-cake the actual source, not the file it is compiled over
The About screen has been showing Cake Wallet's app icon, and the receive QR
page — the screen a user screenshots to get paid — has been showing Cake's
wordmark. In CI builds only, which is why nobody caught it.

compile_graphics.sh compiles res/pictures/*.svg into assets/new-ui/*.svg.vec,
and every platform workflow runs it. That makes res/pictures the source and
assets/new-ui the output. The August de-cake pass replaced three files under
assets/new-ui and left res/pictures untouched, so each CI build faithfully
recompiled Cake's artwork straight over the top of ours. Locally it looked
right, because locally nobody re-runs the compiler.

Three files, all of them branding: hash_squircle_icon.svg, hash-wordmark.svg,
hash-wordmark-light.svg. The sources are now the Hash Bags artwork, so the
compiler produces what the filenames have claimed since August.

This is the third time somebody else's artwork has reached users through this
repo, and the second time an audit missed it, so it gets a test rather than
another careful look: any SVG present in both res/pictures and assets/new-ui
must be byte-identical. Drift means a hand-edit that the next CI build will
silently revert. The test also asserts it compared something, because a silent
zero is how the previous two got through.

AGENTS.md has had the rule the whole time: generated files are never
hand-edited, fix the source and regenerate. The trap here is that the generated
copies sit beside the sources under identical names, and the ones the Dart code
references are the generated ones.
2026-09-24 19:50:00 -04:00

58 lines
2.5 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
// compile_graphics.sh compiles res/pictures/*.svg into assets/new-ui/*.svg.vec,
// and CI runs it on every platform. That makes res/pictures the source and
// assets/new-ui the output, which is easy to get backwards: the files sit side
// by side, have identical names, and the ones under assets/ are the ones the
// Dart code names.
//
// Getting it backwards shipped Cake Wallet's artwork twice. The August de-cake
// pass replaced the copies under assets/new-ui and left res/pictures untouched,
// so every CI build recompiled Cake's icon over the top: their app icon on the
// About screen, and their wordmark on the receive QR page, which is the screen
// a user screenshots to get paid. A local build looked right, because locally
// nobody re-runs the compiler.
//
// So: any SVG present in both places must be byte-identical. If they drift, the
// one under assets/ is a hand-edit that the next CI build will silently revert.
void main() {
test('brand SVGs under assets/new-ui match their res/pictures source', () {
final assetsDir = Directory('assets/new-ui');
final sourceDir = Directory('res/pictures');
expect(assetsDir.existsSync(), isTrue, reason: 'no assets/new-ui — wrong CWD?');
expect(sourceDir.existsSync(), isTrue, reason: 'no res/pictures — wrong CWD?');
final drifted = <String>[];
var compared = 0;
for (final f in assetsDir.listSync().whereType<File>()) {
if (!f.path.endsWith('.svg')) continue;
final name = f.uri.pathSegments.last;
final source = File('${sourceDir.path}/$name');
if (!source.existsSync()) continue; // not compiled from res/pictures
compared++;
final a = f.readAsBytesSync();
final b = source.readAsBytesSync();
if (a.length != b.length || !_same(a, b)) {
drifted.add(
'$name: assets/new-ui is ${a.length}b, res/pictures is ${b.length}b. '
'Edit res/pictures and re-run ./compile_graphics.sh; the assets copy '
'is output and CI overwrites it.',
);
}
}
// A silent zero would make this test vacuous, which is how the last two
// branding regressions reached users.
expect(compared, greaterThan(0), reason: 'compared no SVG pairs at all');
expect(drifted, isEmpty, reason: 'generated brand assets drifted from source:\n${drifted.join('\n')}');
});
}
bool _same(List<int> a, List<int> b) {
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}