fix(learn): the instance asks LanguageTool for its languages again a minute after it didn't answer

Switching the sidecar on learn.halkyon.eu's server recreated the server
beside LanguageTool, whose start takes a minute: the server's first look
at the languages failed, and it looked again only an hour later, as
after one that worked. Until then instance.info() named no languages,
and an app that asked meanwhile keeps that answer, so its editors check
nothing until it starts anew. A look LanguageTool doesn't answer is now
tried again a minute later, by a timer, until it answers, the languages
known before kept meanwhile; one that worked still counts for an hour.

The server's tests take fake_async (^1.3.3, as Learn's app, already in
the lock) for the timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-26 12:32:06 +01:00
commit 19dd4d9c4e
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:clock/clock.dart';
import 'package:fake_async/fake_async.dart';
import 'package:halkyon_learn_server/src/generated/protocol.dart';
import 'package:halkyon_learn_server/src/grammar/grammar_checking.dart';
import 'package:halkyon_learn_server/src/grammar/language_tool.dart';
@ -285,6 +286,51 @@ void main() {
});
});
test('looks again a minute after LanguageTool failed to answer, as it '
'does while starting beside the server, until it answers', () {
fakeAsync((async) {
final fake = _FakeLanguageTool()..fails = true;
final checking = GrammarChecking(languageTool: fake);
unawaited(checking.look());
async.flushMicrotasks();
expect(checking.languages, isNull);
expect(fake.looks, 1);
async.elapse(const Duration(seconds: 61));
expect(fake.looks, 2, reason: 'still failing: again a minute on');
fake.fails = false;
async.elapse(const Duration(seconds: 61));
expect(fake.looks, 3);
expect(checking.languages, ['de', 'en', 'ja']);
async.elapse(const Duration(minutes: 30));
expect(fake.looks, 3, reason: 'answered: nothing until an hour on');
});
});
test('keeps the languages it knows when a later look fails, and looks '
'again a minute after', () {
fakeAsync((async) {
final fake = _FakeLanguageTool();
final checking = GrammarChecking(languageTool: fake);
unawaited(checking.look());
async.elapse(const Duration(minutes: 61));
fake
..fails = true
..codes = ['de-DE', 'en-US', 'ja-JP', 'el-GR'];
expect(checking.languages, ['de', 'en', 'ja']);
async.flushMicrotasks();
expect(fake.looks, 2);
expect(checking.languages, ['de', 'en', 'ja']);
fake.fails = false;
async.elapse(const Duration(seconds: 61));
expect(fake.looks, 3);
expect(checking.languages, ['de', 'el', 'en', 'ja']);
});
});
test('checks in the code the language becomes, nothing for an empty '
'text', () async {
final fake = _FakeLanguageTool();
@ -374,11 +420,15 @@ class _FakeLanguageTool implements LanguageTool {
bool fails = false;
final checked = <(String, String)>[];
/// How often it was asked for its languages.
int looks = 0;
@override
Uri get address => Uri.parse('http://languagetool:8081/');
@override
Future<List<String>> languages() async {
looks++;
if (fails) {
throw const LanguageToolFailed('down');
}