halkyon/test/release/whats_new_test.dart
Tizian 「ティツィアーン」 4239a1de24
build(release): what's new drafted from the commits' Whats-New trailers, other apps' commits left out
The drafts came from the commits' subjects, which are written for
developers: each of the releases of 4 Nov 2026 (Todo 0.2.0, Train 0.3.0,
Learn 0.24.0) listed every commit since the last, Meditate's among them
where it touched a shared package, more than twice F-Droid's 500
characters, the German all still to translate, and each had to be written
again whole.

A commit people would notice now says so in its message's trailers,
`Whats-New: <sentence>` once a change and `Whats-New-De: <Satz>` in the
same order, and the draft is those lines alone, the features' first, a
missing translation marked TRANSLATE:. Only where no commit of the
release carries one does it fall back to the subjects. A commit whose
scope names apps (feat(todo,meditate): ...) counts for those alone, any
other for every app it touches. Over a running next.<language>.txt per
app, which parallel branches conflict on at every merge, and over an
assistant drafting at release time (a key, the commits sent to a
provider, a different text each run). ADR 0030, D61.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-05 08:58:26 +01:00

195 lines
5.9 KiB
Dart

import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import '../../tool/src/release/apps.dart';
import '../../tool/src/release/version.dart';
import '../../tool/src/release/whats_new.dart';
import 'fixtures.dart';
void main() {
test('the drafts list the features, then the fixes, without types, scopes '
'and closing references, as sentences; German marked to translate', () {
final drafts = draftWhatsNew(
const [
Commit(hash: 'a', subject: 'fix(sync): no lost rating'),
Commit(hash: 'b', subject: 'docs: why'),
Commit(
hash: 'c',
subject: 'feat(train): tabs folding into the title (ADR 0030, D60)',
),
Commit(hash: 'd', subject: 'perf!: faster library.'),
Commit(hash: 'e', subject: 'chore(release): notes-v0.18.0'),
],
first: false,
app: 'notes',
apps: _apps,
);
expect(drafts['en'], '''
- Tabs folding into the title.
- No lost rating.
- Faster library.
''');
expect(drafts['de'], '''
- TRANSLATE: Tabs folding into the title.
- TRANSLATE: No lost rating.
- TRANSLATE: Faster library.
''');
});
test('the first release says so', () {
expect(
draftWhatsNew(const [], first: true, app: 'notes', apps: _apps)['en'],
'- The first release.\n',
);
});
test("commits scoped to other apps don't count; a shared package's do", () {
final drafts = draftWhatsNew(
const [
Commit(hash: 'a', subject: 'feat(diary): the first diary'),
Commit(hash: 'b', subject: 'feat(notes,diary): a lock on both'),
Commit(hash: 'c', subject: 'feat(app): a profile everywhere'),
Commit(hash: 'd', subject: 'fix(deploy): the image builds'),
],
first: false,
app: 'notes',
apps: _apps,
);
expect(drafts['en'], '''
- A lock on both.
- A profile everywhere.
- The image builds.
''');
});
group('the trailers', () {
const commits = [
Commit(
hash: 'a',
subject: 'fix(notes): the sync no longer loses a rating',
body:
'Why it did.\n\n'
'Whats-New: Ratings are kept when syncing.\n'
'Whats-New-De: Bewertungen bleiben beim Synchronisieren.\n'
'Signed-off-by: A <a@example.org>\n',
),
Commit(hash: 'b', subject: 'feat(notes): a refactor nobody sees'),
Commit(
hash: 'c',
subject: 'feat(app): tabs folding into the title (ADR 0030, D60)',
body:
'Whats-New: Tabs fold into the title.\n'
'Whats-New: Search: in every list.\n'
'Whats-New-De: Tabs falten sich in den Titel.\n',
),
Commit(
hash: 'd',
subject: 'feat(diary): a diary',
body: 'Whats-New: A diary.\nWhats-New-De: Ein Tagebuch.\n',
),
];
test("are what's new where a commit has them: the features' first, "
'commits without them left out, those of other apps too', () {
final drafts = draftWhatsNew(
commits,
first: false,
app: 'notes',
apps: _apps,
);
expect(drafts['en'], '''
- Tabs fold into the title.
- Search: in every list.
- Ratings are kept when syncing.
''');
expect(drafts['de'], '''
- Tabs falten sich in den Titel.
- TRANSLATE: Search: in every list.
- Bewertungen bleiben beim Synchronisieren.
''');
});
test('are left alone in the first release', () {
expect(
draftWhatsNew(commits, first: true, app: 'notes', apps: _apps)['de'],
'- TRANSLATE: The first release.\n',
);
});
});
test("a text's changes are its lines starting with '- '", () {
expect(whatsNewChanges('- One\n\n - Two \n'), ['One', 'Two']);
expect(whatsNewList('- One\n\n - Two \n'), '- One\n- Two');
});
group('problems', () {
test('none in an edited text', () {
expect(whatsNewProblems('x', '- One.\n- Two.\n'), isEmpty);
});
test('a text listing nothing', () {
expect(whatsNewProblems('x', '\n'), ['x lists no change.']);
});
test('a line not starting with a dash', () {
expect(whatsNewProblems('x', '- One.\nTwo.\n'), [
"x, line 2: doesn't start with '- '.",
]);
});
test('a line still to be translated', () {
expect(whatsNewProblems('x', '- TRANSLATE: One.\n'), [
'x is still to be translated (TRANSLATE:).',
]);
});
test("more than F-Droid's 500 characters", () {
expect(whatsNewProblems('x', '- ${'a' * 499}\n'), [
"x has 501 characters, more than F-Droid's 500.",
]);
});
});
test("the files are the app's, F-Droid's changelog its listing's", () {
expect(
whatsNewFile(testApp, const Version(0, 19, 0), 'de'),
'apps/notes/halkyon_notes_flutter/assets/whats_new/0.19.0.de.txt',
);
expect(
fdroidChangelogFile(testApp, 23, 'en'),
'app.halkyon.notes/en-US/changelogs/23.txt',
);
expect(
fdroidChangelogFile(testApp, 23, 'de'),
'app.halkyon.notes/de/changelogs/23.txt',
);
});
// The texts every app bundles, as a release would take them.
for (final app in releasedApps.values) {
test("${app.name}'s texts of what's new are each release's in every "
'language, and as a release takes them', () {
final folder = Directory(app.whatsNewDirectory);
final byVersion = <String, Set<String>>{};
for (final file in folder.listSync().whereType<File>()) {
final name = p.basename(file.path);
final match = whatsNewName.firstMatch(name);
expect(match, isNotNull, reason: '$name: <version>.<language>.txt');
(byVersion[match![1]!] ??= {}).add(match[2]!);
expect(whatsNewProblems(name, file.readAsStringSync()), isEmpty);
}
expect(byVersion, isNotEmpty);
for (final MapEntry(key: version, value: languages)
in byVersion.entries) {
expect(languages, whatsNewLanguages.toSet(), reason: version);
}
});
}
}
const _apps = {'notes', 'diary'};